I’m using the -[NSFontDescriptor matchingFontDescriptorsWithMandatoryKeys:] method in order to find if a font is already installed in the system but I get very strange results.
Here is a small program to test if a given font matches an installed font:
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
void logFontDescriptor(NSString *message, NSFontDescriptor *fontDescriptor, NSArray *attributeKeys)
{
printf("%s %s\n", [message UTF8String], [fontDescriptor.postscriptName UTF8String]);
for (NSString *attributeKey in attributeKeys)
{
printf(" %30s -> %s\n", [[attributeKey description] UTF8String], [[[fontDescriptor objectForKey:attributeKey] description] UTF8String]);
}
printf("\n");
}
int main(int argc, const char * argv[])
{
#autoreleasepool
{
if (argc != 2)
return EXIT_FAILURE;
NSArray *mandatoryKeys = #[ NSFontNameAttribute, NSFontFamilyAttribute, NSFontFaceAttribute ];
NSURL *fontURL = [NSURL fileURLWithPath:#(argv[1])];
NSArray *fontDescriptors = CFBridgingRelease(CTFontManagerCreateFontDescriptorsFromURL((__bridge CFURLRef)fontURL));
for (NSFontDescriptor *fontDescriptor in fontDescriptors)
{
logFontDescriptor(#"***", fontDescriptor, mandatoryKeys);
NSArray *matchingFontDescriptors = [fontDescriptor matchingFontDescriptorsWithMandatoryKeys:[NSSet setWithArray:mandatoryKeys]];
for (NSFontDescriptor *matchingFontDescriptor in matchingFontDescriptors)
{
logFontDescriptor(#" MATCHING", matchingFontDescriptor, mandatoryKeys);
}
}
}
return EXIT_SUCCESS;
}
In order to reproduce the strange behavior, download the Ubuntu Font Family and copy only the Ubuntu-B.ttf (Ubuntu Bold) file into your ~/Library/Fonts directory. Then run this test program with Ubuntu-R.ttf (Ubuntu Regular):
./font_descriptor_matching ~/Downloads/ubuntu-font-family-0.80/Ubuntu-R.ttf
Now, you have the Ubuntu Bold font installed on your system and you are asking what font descriptors are matching Ubuntu Regular and here is the result (OS X 10.9.1):
*** Ubuntu
NSFontNameAttribute -> Ubuntu
NSFontFamilyAttribute -> Ubuntu
NSFontFaceAttribute -> Regular
MATCHING Ubuntu-Bold
NSFontNameAttribute -> Ubuntu-Bold
NSFontFamilyAttribute -> Ubuntu
NSFontFaceAttribute -> Bold
Asking Ubuntu Regular what font matches its font name (NSFontNameAttribute), font family (NSFontFamilyAttribute) and font face (NSFontFaceAttribute) yields that the Ubuntu Bold font matches. As you can see, the only attribute that matches is the font family. Both the font name and font face are different yet the matchingFontDescriptorsWithMandatoryKeys: method says that these fonts match.
Am I misunderstanding what the matchingFontDescriptorsWithMandatoryKeys: method does or is it a bug?
Additional Information:
If both the Ubuntu Bold and the Ubuntu Regular fonts are installed then the matching works as expected.
This issue does not exist with all fonts.
In order to workaround this odd behavior, I used this method which suppresses non matching font descriptors by explicitly testing all mandatory keys.
static NSArray * ReallyMatchingFontDescriptorsWithMandatoryKeys(NSFontDescriptor *fontDescriptor, NSSet *mandatoryKeys)
{
NSArray *matchingFontDescriptors = [fontDescriptor matchingFontDescriptorsWithMandatoryKeys:mandatoryKeys];
NSMutableArray *reallyMatchingFontDescriptors = [matchingFontDescriptors mutableCopy];
for (NSFontDescriptor *matchingFontDescriptor in matchingFontDescriptors)
{
BOOL reallyMatching = YES;
for (NSString *mandatoryKey in mandatoryKeys)
{
if (![[fontDescriptor objectForKey:mandatoryKey] isEqual:[matchingFontDescriptor objectForKey:mandatoryKey]])
reallyMatching = NO;
}
if (!reallyMatching)
[reallyMatchingFontDescriptors removeObjectIdenticalTo:matchingFontDescriptor];
}
return [reallyMatchingFontDescriptors copy];
}
Related
I'd like to implement XCTest unit-tests, but don't really want to use XCode to do it. It's not obvious how to do this, and I'm wondering if this is even possible?
From what I've found so far, one could get the impression that XCTest is completely dependent on XCode. I've found the xcodebuild test command-line support, but that depends on finding an XCode project or workspace.
Have I any options here, or do I just rip out the existing SenTestingKit code and revert to some home-brew unit test code? I have some such code to hand, but it's not the Right Thing To Do.
Rationale/history:
This is not just me being old-skool. I have an Objective-C program which I last touched two years ago, for which I had developed a reasonable set of unit tests based on SenTestingKit. Now I come back to this code – I may at least have to rebuild the thing, because of intervening library changes – I discover that SenTestingKit has disappeared, to be replaced by XCTest. Oh well....
This code was not developed using XCode, so there isn't a .project file associated with it, and the tests were up to now happily managed using SenTestingKit's main programs, and a Makefile check target (that's partly being old-skool, again, partly a lack of fondness for IDEs, and partly this having been an experiment with Objective-C, so originally sticking with what I know).
Thanks, #stanislaw-pankevich, for a great answer. Here, for completeness, I'm including (more-or-less) the complete test program which I ended up with, which includes a couple of extra details and comments.
(This is a complete program from my point of view, since it tests functions
defined in util.h, which isn't included here)
File UtilTest.h:
#import <XCTest/XCTest.h>
#interface UtilTest : XCTestCase
#end
File UtilTest.m:
#import "UtilTest.h"
#import "../util.h" // the definition of the functions being tested
#implementation UtilTest
// We could add methods setUp and tearDown here.
// Every no-arg method which starts test... is included as a test-case.
- (void)testPathCanonicalization
{
XCTAssertEqualObjects(canonicalisePath("/p1/./p2///p3/..//f3"), #"/p1/p2/f3");
}
#end
Driver program runtests.m (this is the main program, which the makefile actually invokes to run all the tests):
#import "UtilTest.h"
#import <XCTest/XCTestObservationCenter.h>
// Define my Observation object -- I only have to do this in one place
#interface BrownieTestObservation : NSObject<XCTestObservation>
#property (assign, nonatomic) NSUInteger testsFailed;
#property (assign, nonatomic) NSUInteger testsCalled;
#end
#implementation BrownieTestObservation
- (instancetype)init {
self = [super init];
self.testsFailed = 0;
return self;
}
// We can add various other functions here, to be informed about
// various events: see XCTestObservation at
// https://developer.apple.com/reference/xctest?language=objc
- (void)testSuiteWillStart:(XCTestSuite *)testSuite {
NSLog(#"suite %#...", [testSuite name]);
self.testsCalled = 0;
}
- (void)testSuiteDidFinish:(XCTestSuite *)testSuite {
NSLog(#"...suite %# (%tu tests)", [testSuite name], self.testsCalled);
}
- (void)testCaseWillStart:(XCTestSuite *)testCase {
NSLog(#" test case: %#", [testCase name]);
self.testsCalled++;
}
- (void)testCase:(XCTestCase *)testCase didFailWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber {
NSLog(#" FAILED: %#, %# (%#:%tu)", testCase, description, filePath, lineNumber);
self.testsFailed++;
}
#end
int main(int argc, char** argv) {
XCTestObservationCenter *center = [XCTestObservationCenter sharedTestObservationCenter];
BrownieTestObservation *observer = [BrownieTestObservation new];
[center addTestObserver:observer];
Class classes[] = { [UtilTest class], }; // add other classes here
int nclasses = sizeof(classes)/sizeof(classes[0]);
for (int i=0; i<nclasses; i++) {
XCTestSuite *suite = [XCTestSuite testSuiteForTestCaseClass:classes[i]];
[suite runTest];
}
int rval = 0;
if (observer.testsFailed > 0) {
NSLog(#"runtests: %tu failures", observer.testsFailed);
rval = 1;
}
return rval;
}
Makefile:
FRAMEWORKS=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks
TESTCASES=UtilTest
%.o: %.m
clang -F$(FRAMEWORKS) -c $<
check: runtests
./runtests 2>runtests.stderr
runtests: runtests.o $(TESTCASES:=.o) ../libmylib.a
cc -o $# $< -framework Cocoa -F$(FRAMEWORKS) -rpath $(FRAMEWORKS) \
-framework XCTest $(TESTCASES:=.o) -L.. -lmylib
Notes:
The XCTestObserver class is now deprecated, and replaced by XCTestObservation.
The results of tests are sent to a shared XCTestObservationCenter, which unfortunately chatters distractingly to stderr (which therefore has to be redirected elsewhere) – it doesn't seem possible to avoid that and have them sent only to my observation centre instead. In my actual program, I replaced the NSLog calls in runtests.m with a function which chatters to stdout, which I could therefore distinguish from the chatter going to the default ObservationCenter.
See also the overview documentation
(presumes that you're using XCode),
...the XCTest API documentation,
...and the notes in the headers of the files at (eg) /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework/Headers
If you are looking for Xcode-based solutions see this and its linked solutions for examples.
For complete non-Xcode-based solution continue reading.
I used to ask similar answer a few years ago: Is there any non-Xcode-based command line unit testing tool for Objective-C? but things changed since then.
One interesting feature that appeared in XCTest over time is ability to run your custom test suites. I used to implement them successfully for my research needs, here is an example code which is a command line Mac OS application:
#interface FooTest : XCTestCase
#end
#implementation FooTest
- (void)testFoo {
XCTAssert(YES);
}
- (void)testFoo2 {
XCTAssert(NO);
}
#end
#interface TestObserver : NSObject <XCTestObservation>
#property (assign, nonatomic) NSUInteger testsFailed;
#end
#implementation TestObserver
- (instancetype)init {
self = [super init];
self.testsFailed = 0;
return self;
}
- (void)testBundleWillStart:(NSBundle *)testBundle {
NSLog(#"testBundleWillStart: %#", testBundle);
}
- (void)testBundleDidFinish:(NSBundle *)testBundle {
NSLog(#"testBundleDidFinish: %#", testBundle);
}
- (void)testSuiteWillStart:(XCTestSuite *)testSuite {
NSLog(#"testSuiteWillStart: %#", testSuite);
}
- (void)testCaseWillStart:(XCTestCase *)testCase {
NSLog(#"testCaseWillStart: %#", testCase);
}
- (void)testSuiteDidFinish:(XCTestSuite *)testSuite {
NSLog(#"testSuiteDidFinish: %#", testSuite);
}
- (void)testSuite:(XCTestSuite *)testSuite didFailWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber {
NSLog(#"testSuite:didFailWithDescription:inFile:atLine: %# %# %# %tu",
testSuite, description, filePath, lineNumber);
}
- (void)testCase:(XCTestCase *)testCase didFailWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber {
NSLog(#"testCase:didFailWithDescription:inFile:atLine: %# %# %# %tu",
testCase, description, filePath, lineNumber);
self.testsFailed++;
}
- (void)testCaseDidFinish:(XCTestCase *)testCase {
NSLog(#"testCaseWillFinish: %#", testCase);
}
#end
int RunXCTests() {
XCTestObserver *testObserver = [XCTestObserver new];
XCTestObservationCenter *center = [XCTestObservationCenter sharedTestObservationCenter];
[center addTestObserver:testObserver];
XCTestSuite *suite = [XCTestSuite defaultTestSuite];
[suite runTest];
NSLog(#"RunXCTests: tests failed: %tu", testObserver.testsFailed);
if (testObserver.testsFailed > 0) {
return 1;
}
return 0;
}
To compile this kind of code you will need to show a path to the folder where XCTest is located something like:
# in your Makefile
clang -F/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks XCTestDriver.m
Don't expect the code to compile but it should give you an idea. Feel free to ask if you have any questions. Also follow the headers of XCTest framework to learn more about its classes and their docs.
This one works ok:
Build your .xctest target as usual. By default it will be added to Plugins of the host app, but the location is irrelevant.
Create a runner command line tool with the code below.
Update: Xcode ships runner that is fairly standalone in /Applications/Xcode.app/Contents/Developer/usr/bin/xctest
You may use this one, if you don't want to create your own simple runner.
Run the tool with the full path to your test suite.
Sample runner code:
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
int main(int argc, const char* argv[]) {
#autoreleasepool {
XCTestSuite *suite = [XCTestSuite testSuiteForBundlePath:
[NSString stringWithUTF8String:argv[1]]];
[suite runTest];
// Note that XCTestSuite is very shy in terms of errors,
// so make sure that it loaded anything indeed:
if (!suite.testRun.testCaseCount) return 1;
return suite.testRun.hasSucceeded;
}
}
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)
}
})
}
}
}
I'm trying to figure out how to set the default web browser on OS X via command line. I found this forum post, but the solution they are working out there is to open a specific URL with a specific application. I am looking for a way to set the default browser system wide. There's this neat app on GitHub, named Objektiv, which does exactly what I am after, but it's an app. There's a Cocoa method, apparently, to set the NSWorkSpace default browser.
The preference file com.apple.LaunchServices needs to be changed for this, maybe more.
How to set a different default browser via command line?
Thanks for help.
I found this tool. After installing it, you can do this:
defaultbrowser chrome
Here's the entire source-code in case the link 404s in the future. It is licensed under the MIT license with Copyright (c) 2014 Margus Kerma.
#import <Foundation/Foundation.h>
#import <ApplicationServices/ApplicationServices.h>
NSString* app_name_from_bundle_id(NSString *app_bundle_id) {
return [[app_bundle_id componentsSeparatedByString:#"."] lastObject];
}
NSMutableDictionary* get_http_handlers() {
NSArray *handlers =
(__bridge NSArray *) LSCopyAllHandlersForURLScheme(
(__bridge CFStringRef) #"http"
);
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < [handlers count]; i++) {
NSString *handler = [handlers objectAtIndex:i];
dict[[app_name_from_bundle_id(handler) lowercaseString]] = handler;
}
return dict;
}
NSString* get_current_http_handler() {
NSString *handler =
(__bridge NSString *) LSCopyDefaultHandlerForURLScheme(
(__bridge CFStringRef) #"http"
);
return app_name_from_bundle_id(handler);
}
void set_default_handler(NSString *url_scheme, NSString *handler) {
LSSetDefaultHandlerForURLScheme(
(__bridge CFStringRef) url_scheme,
(__bridge CFStringRef) handler
);
}
int main(int argc, const char *argv[]) {
const char *target = (argc == 1) ? '\0' : argv[1];
#autoreleasepool {
// Get all HTTP handlers
NSMutableDictionary *handlers = get_http_handlers();
// Get current HTTP handler
NSString *current_handler_name = get_current_http_handler();
if (target == '\0') {
// List all HTTP handlers, marking the current one with a star
for (NSString *key in handlers) {
char *mark = [key isEqual:current_handler_name] ? "* " : " ";
printf("%s%s\n", mark, [key UTF8String]);
}
} else {
NSString *target_handler_name = [NSString stringWithUTF8String:target];
if ([target_handler_name isEqual:current_handler_name]) {
printf("%s is already set as the default HTTP handler\n", target);
} else {
NSString *target_handler = handlers[target_handler_name];
if (target_handler != nil) {
// Set new HTTP handler (HTTP and HTTPS separately)
set_default_handler(#"http", target_handler);
set_default_handler(#"https", target_handler);
} else {
printf("%s is not available as an HTTP handler\n", target);
return 1;
}
}
}
}
return 0;
}
Makefile:
BIN ?= defaultbrowser
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
CC = gcc
CFLAGS = -O2
.PHONY: all install uninstall clean
all:
gcc -o $(BIN) $(CFLAGS) -framework Foundation -framework ApplicationServices src/main.m
install: all
install -d $(BINDIR)
install -m 755 $(BIN) $(BINDIR)
uninstall:
rm -f $(BINDIR)/$(BIN)
clean:
rm -f $(BIN)
I know this doesn't specifically answer your question, but if you only need to do this for Chrome, it has a flag that let's you set itself as the default browser:
$ open -a "Google Chrome" --args --make-default-browser
For newer versions of macOS (Catalina, Big Sur) I re-implemented the default browser tool in Swift (called it defbro). Main reason was to use a "modern" programming language that gives me an easy way to tweak and improve it. I also added json output defbro --json, in case you want to do other stuff with it.
Installation: brew install jwbargsten/misc/defbro
Repo: https://github.com/jwbargsten/defbro
The only way is to set it in the main Settings:
From the Apple menu, choose System Preferences, then click
General.
Click the “Default web browser” pop-up menu and choose
a web browser, like Chrome.
I would like to read a simple text file in objective-C (command line tool) this is my code
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[])
{
#autoreleasepool {
// insert code here...
NSLog(#"Hello, World!");
NSBundle *bundle = [NSBundle mainBundle];
NSString *aPath = [bundle pathForResource:#"data" ofType:#"txt"];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:aPath];
if (fileExists) {
NSLog(#"THERE!");
} else {
NSLog(#"NOT THERE!");
}
but I tried full path like /Users/Me/data or added data.txt into xcode but it just won't load the file, what am I missing? thank you!
Command-line tools don't have a bundle associated with them. You'll have to store the file somewhere else (a directory in /Library/Application Support, for example, or perhaps somewhere in /usr/local/share if you're installing the tool to /usr/local) and read it from there.
Sample code is
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:#selector(activeSpaceDidChange:) name:NSWorkspaceActiveSpaceDidChangeNotification object:nil];
Then
- (void) activeSpaceDidChange:(NSNotification *)aNotification {
// code to check if current workspace is dashboard?
}
I want to check whether the current space is dashboard or not? Any idea?
The first think i have tried is get to the current space id according to this answer: Detecting when a space changes in Spaces in Mac OS X . The problem here is that the key kCGWindowWorkspace is deprecated in OSX 10.8. So there is no direct way to get this information.
In my solution now i check for different windows or owners which are only one the dashboard space or on all other spaces:
The user is on the dashboard if there is one window which kCGWindowName ends with .wdgt/
The user is not on the dashboard if there is one window with kCGWindowName == System Status Item Clone, kCGWindowOwnerName == SystemUIServer | Finder
So why i'm not just using the .wdgt/ check? -- Because if there is now widget on the dashboard this not working
So why i'm using more than one window check? -. Because i'm not jet sure which window is always on all spaces. At least System Status Item Clone and Finder are not always there.
Here my implementation is add this function as category to NSWorkspace
- (BOOL) userIsOnDashboardSpace {
NSArray* windowsInSpace = (__bridge NSArray *) CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSUInteger indexOfWidget = [windowsInSpace indexOfObjectPassingTest:^BOOL(NSDictionary* obj, NSUInteger idx, BOOL *stop) {
if ([obj objectForKey:(id)kCGWindowName]) {
NSString *name = (NSString *)[obj objectForKey:(id)kCGWindowName];
if ([name isEqualToString:#"System Status Item Clone"]) {
*stop = true;
return false;
}
if ([name hasSuffix:#".wdgt/"]) {
*stop = true;
return true;
}
}
if ([obj objectForKey:(id)kCGWindowOwnerName]) {
NSString *name = (NSString *)[obj objectForKey:(id)kCGWindowOwnerName];
if ([name isEqualToString:#"SystemUIServer"]) {
*stop = true;
return false;
}
if ([name isEqualToString:#"Finder"]) {
*stop = true;
return false;
}
}
return false;
}];
return indexOfWidget != NSNotFound;
}