My Second Mac App

在 macOS 中我用過好幾個字典,它們都有一個共同的問題:功能太多…

我是一個 minimalist,最怕看到一大堆沒用的 features.. 所以我決定寫一隻我喜歡的字典,其特色如下:

  • 可長註 menu bar
  • login 時可 auto start
  • 用 Safari 時可以 short cut 查詢已 highlight 的字
  • UI 要簡潔

簡單講,就是快、靚、正!

而我覺得我做到了,是很喜歡的一隻 app.

Btw, Apple 經過幾年來的不斷改善,用 Swift 和 Interface Builder 來寫 macOS App 感覺已經變得非常良好。

寫 Mac App 最開心是不用針對不同的 screen size / device 做 optimisation 和 testing,這是比寫 iOS App 爽很多的地方。

另外值得一讚的是,Apple 現在的審批速度快了很多,基本上一天便有回覆,比以往等足一星期,實在是有效率多了~

EasyDictionaryMac1

Download: http://itunes.apple.com/app/id1242356475

My Second Mac App

UI Preparation of Universal Apps

To me, one of the most un-willing task for making universal iOS apps is to prepare the UI of iPad. Because I have to re-do everything that I’ve done for the iPhone UI, such as putting UI elements, setting UI constrains, connecting IBOutlets, setting IBActions…etc. I just boringly repeating my self…

Xcode didn’t provide any function to “migrate” an iPhone storyboard to iPad. However I saw a thread in Stack Overflow discussing how to do it manually. After experimenting myself, here summarised what I’ve done:

  1. Duplicate the iPhone storyboard and rename it Main_iPad.storyboard

  2. Right click and choose “Open As” -> “Source Code”

  3. Search for targetRuntime=”iOS.CocoaTouch”and change it to targetRuntime=”iOS.CocoaTouch.iPad”

  4. Replace <simulatedScreenMetrics key=”destination” type=”retina4″/> to <simulatedScreenMetrics key=”destination”/>

  5. Save everything and restart Xcode.

  6. In your target’s General tab, choose your newly edited storyboard in the iPad’s “Main Interface” setting.

Though we still need to adjust the size of the UI elements to fit into the iPad screen size, it saves us tremendous amount of time as all the outlets / connections / UI elements / constrains are there already!!

UI Preparation of Universal Apps

Play Audio While the Silent Switch is ON

I’d receive emails from time to time complaining my apps that have no sound. However, its actually caused by the user turing on the Silent Switch…

To enable audio playing even when the user turned on the silent switch. Simply put the following code in your app delegate while its launching.

AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
[audioSession setActive:YES error:nil];

The AVAudioSessionCategoryPlayback category enable your app to play audio while silent switch is on (and even the screen is locked). AVAudioSessionCategoryOptionMixWithOthers allow your audio to mix with other audio from other apps, and of course, its optional.

Play Audio While the Silent Switch is ON

Mute Checking in iOS7

Up to iOS 7 there is no simple API to check whether the user switched on the mute button(silent switch) or not…

Sharkfood produced a class to constantly check if a user switched on the mute button.

The idea is playing a short(0.2 sec) muted sound file using AudioServices. If it takes longer then 0.2 sec to complete, mute button is off. Otherwise if it finished very soon, say shorter then 0.1 sec, mute button is on.

However, in most case I don’t need to keep checking mute in the run loop. I just want to know if it is muted while the user start playing some audio. So I modified it and made a class, named MuteChecker, to check it on-demand.

Also, it will call a completion block after checking. So you can put whatever logic you like for the checking result.

To use it, you just simply initialise it with a completion block.

self.muteChecker = [[MuteChecker alloc] initWithCompletionBlk:^(NSTimeInterval lapse, BOOL muted) {
    //your logic here...
}];

The “lapse” parameter return the time used for playing the checking sound.

The “muted” parameter will return YES if the time lapse is < 0.1 sec.

To start checking, call

[_muteChecker check];

and your result will be reflected by the completion block.

You can download a sample project here.

Mute Checking in iOS7

NSUserDefaults KVO in iOS7

Though its not well documented, NSUserDefaults do support key-value observing in iOS7. The following code works:

- (void)viewDidLoad{
  [super viewDidLoad];
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  [ud addObserver:self forKeyPath:@"bar" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
  [ud setObject:@"foo" forKey:@"bar"];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
  NSLog(@"%@", change);
}

Its much more convenient then using NSUserDefaultsDidChangeNotification, since we do know exactly which key is changing.

As the hardware of mobile devices become more and more powerful, I hope the support of key binding could be actualised in near future 🙂

NSUserDefaults KVO in iOS7

Objective C Property Attributes

Just read a post about Objective C property attributes. I wanna make some extra notes on this topic:

1. “weak” vs “assign”

We generally use weak for Objective-C objects that we don’t need to retain, e.g.

@property (weak) IBOutlet UIButton aButton;

because its always good to nil an object if it has been deallocated.

On the other hand, we could use assign for C primitives, e.g.

@property BOOL isGood;

because C primitives has nothing to do with retain count and deallocation, simply assign the value is good enough.

Note that assign is the default property attribute, we can skip it for C primitives for cleaner code.

2. The default attribute of property vs variable

There are 2 kinds of default: property and variable

For property, the default attribute is “assign”

For variable (ivar or local variable), the default ownership qualifier is “__strong”

Don’t mix up “strong” in property and “__strong” in variable. They are different things (see below).

Also remember the default property attribute is “assign”, not “strong”

3. Implicit attribute for the default

The explicit version of

@property BOOL isGood;

should be:

@property (atomic, assign, readwrite) BOOL isGood

so skipping the attributes by using the default is fine for C primitives in most cases.

However, we better explicitly state the attributes for Objective-C objects. Because we seldom want “assign” for objects.

For ivar and local variable,

NSString *str;

is equivalent to

__strong NSString *str;

so if you set it as an ivar, the compiler will automatically retain it for you when you assign a value for it. Which behave differently from not using ARC.

4. Relationship between property attributes and ownership qualifiers

Here listed their relationships according to the official documentation

  • assign implies __unsafe_unretained ownership.
  • copy implies __strong ownership, as well as the usual behavior of copy semantics on the setter.
  • retain implies __strong ownership.
  • strong implies __strong ownership.
  • unsafe_unretained implies __unsafe_unretained ownership.
  • weak implies __weak ownership.

So for property attributes in ARC, practically we can assume assign = unsafe_unretained, and strong = retain.

Objective C Property Attributes

GData Static Library for Specific API

From my previous posts I mentioned how to deploy GData by using all-in-one static library, and official build procedures. But both methods have their trade-offs:

  • Using all-in-one static library is easy but need to import a big image file.
  • Following official procedures generate small image file but tedious. Also, you need to re-compile hundreds of source file every time  after cleaning your build, which is quite clumsy if you are under active development.

So the question is: How can we generate GData static library for specific API, such that its reasonably small and save some re-compiling time after cleaning the build? Here is how:

  • Download GData Objective-C client and open its project in XCode
  • Select the Build Settings of GDataTouchStaticLib target
  • Add -DGDATA_REQUIRE_SERVICE_INCLUDES=1 in the Debug setting of “Other C Flags”

  • Suppose I want to implement Youtube API only, add -DGDATA_INCLUDE_YOUTUBE_SERVICE=1 in both Debug and Release setting of “Other C Flags”. Just change this entry to other API name (say -DGDATA_INCLUDE_CONTACTS_SERVICE=1) if you want to implement other API. You can see all the API names in GData Srouces folder in the source tree.
  • In Build Phases of the same target, click Add Build Phase at the lower right corner and select Add Run Script. Then paste the following shell script:
# Version 2.0 (updated for Xcode 4, with some fixes)
# Changes:
#    - Works with xcode 4, even when running xcode 3 projects (Workarounds for apple bugs)
#    - Faster / better: only runs lipo once, instead of once per recursion
#    - Added some debugging statemetns that can be switched on/off by changing the DEBUG_THIS_SCRIPT variable to "true"
#    - Fixed some typos
#
# Purpose:
#   Create a static library for iPhone from within XCode
#   Because Apple staff DELIBERATELY broke Xcode to make this impossible from the GUI (Xcode 3.2.3 specifically states this in the Release notes!)
#   ...no, I don't understand why they did this!
#
# Author: Adam Martin - http://twitter.com/redglassesapps
# Based on: original script from Eonil (main changes: Eonil's script WILL NOT WORK in Xcode GUI - it WILL CRASH YOUR COMPUTER)
#
# More info: see this Stack Overflow question: http://stackoverflow.com/questions/3520977/build-fat-static-library-device-simulator-using-xcode-and-sdk-4

#################[ Tests: helps workaround any future bugs in Xcode ]########
#
DEBUG_THIS_SCRIPT="false"

if [ $DEBUG_THIS_SCRIPT = "true" ]
then
echo "########### TESTS #############"
echo "Use the following variables when debugging this script; note that they may change on recursions"
echo "BUILD_DIR = $BUILD_DIR"
echo "BUILD_ROOT = $BUILD_ROOT"
echo "CONFIGURATION_BUILD_DIR = $CONFIGURATION_BUILD_DIR"
echo "BUILT_PRODUCTS_DIR = $BUILT_PRODUCTS_DIR"
echo "CONFIGURATION_TEMP_DIR = $CONFIGURATION_TEMP_DIR"
echo "TARGET_BUILD_DIR = $TARGET_BUILD_DIR"
fi

#####################[ part 1 ]##################
# First, work out the BASESDK version number (NB: Apple ought to report this, but they hide it)
#    (incidental: searching for substrings in sh is a nightmare! Sob)

SDK_VERSION=$(echo ${SDK_NAME} | grep -o '.\{3\}$')

# Next, work out if we're in SIM or DEVICE

if [ ${PLATFORM_NAME} = "iphonesimulator" ]
then
OTHER_SDK_TO_BUILD=iphoneos${SDK_VERSION}
else
OTHER_SDK_TO_BUILD=iphonesimulator${SDK_VERSION}
fi

echo "XCode has selected SDK: ${PLATFORM_NAME} with version: ${SDK_VERSION} (although back-targetting: ${IPHONEOS_DEPLOYMENT_TARGET})"
echo "...therefore, OTHER_SDK_TO_BUILD = ${OTHER_SDK_TO_BUILD}"
#
#####################[ end of part 1 ]##################

#####################[ part 2 ]##################
#
# IF this is the original invocation, invoke WHATEVER other builds are required
#
# Xcode is already building ONE target...
#
# ...but this is a LIBRARY, so Apple is wrong to set it to build just one.
# ...we need to build ALL targets
# ...we MUST NOT re-build the target that is ALREADY being built: Xcode WILL CRASH YOUR COMPUTER if you try this (infinite recursion!)
#
#
# So: build ONLY the missing platforms/configurations.

if [ "true" == ${ALREADYINVOKED:-false} ]
then
echo "RECURSION: I am NOT the root invocation, so I'm NOT going to recurse"
else
# CRITICAL:
# Prevent infinite recursion (Xcode sucks)
export ALREADYINVOKED="true"

echo "RECURSION: I am the root ... recursing all missing build targets NOW..."
echo "RECURSION: ...about to invoke: xcodebuild -configuration \"${CONFIGURATION}\" -target \"${TARGET_NAME}\" -sdk \"${OTHER_SDK_TO_BUILD}\" ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO"
xcodebuild -configuration "${CONFIGURATION}" -target "${TARGET_NAME}" -sdk "${OTHER_SDK_TO_BUILD}" ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}"

ACTION="build"

#Merge all platform binaries as a fat binary for each configurations.

# Calculate where the (multiple) built files are coming from:
CURRENTCONFIG_DEVICE_DIR=${SYMROOT}/${CONFIGURATION}-iphoneos
CURRENTCONFIG_SIMULATOR_DIR=${SYMROOT}/${CONFIGURATION}-iphonesimulator

echo "Taking device build from: ${CURRENTCONFIG_DEVICE_DIR}"
echo "Taking simulator build from: ${CURRENTCONFIG_SIMULATOR_DIR}"

CREATING_UNIVERSAL_DIR=${SYMROOT}/${CONFIGURATION}-universal
echo "...I will output a universal build to: ${CREATING_UNIVERSAL_DIR}"

# ... remove the products of previous runs of this script
#      NB: this directory is ONLY created by this script - it should be safe to delete!

rm -rf "${CREATING_UNIVERSAL_DIR}"
mkdir "${CREATING_UNIVERSAL_DIR}"

#
echo "lipo: for current configuration (${CONFIGURATION}) creating output file: ${CREATING_UNIVERSAL_DIR}/${EXECUTABLE_NAME}"
lipo -create -output "${CREATING_UNIVERSAL_DIR}/${EXECUTABLE_NAME}" "${CURRENTCONFIG_DEVICE_DIR}/${EXECUTABLE_NAME}" "${CURRENTCONFIG_SIMULATOR_DIR}/${EXECUTABLE_NAME}"

#########
#
# Added: StackOverflow suggestion to also copy "include" files
#    (untested, but should work OK)
#
if [ -d "${CURRENTCONFIG_DEVICE_DIR}/usr/local/include" ]
then
mkdir -p "${CREATING_UNIVERSAL_DIR}/usr/local/include"
# * needs to be outside the double quotes?
cp "${CURRENTCONFIG_DEVICE_DIR}/usr/local/include/"* "${CREATING_UNIVERSAL_DIR}/usr/local/include"
fi
fi
  • Build the project (⌘B), then get the build products by clicking the little arrow in Organizer -> Projects -> Derived Data. There should be 3 directories (iphoneos, simulator, and universal) in your project’s Build/Products directory. Copy out the Headers subdirectory from iphoneos, and libGDataTouchStaticLib.a from the universal directory.

Thats all! You now have your GData static library for specific API. Its reasonably small and easy to use, just like other static library you download elsewhere ~~

Here is how to use your static library:

  • Drag the headers and library image into your project
  • In your target’s Build Phases -> Link Binary With Libraries, add libxml2.dylib
  • In your target’s Build Settings, set /usr/include/libxml2 in “Header Search Paths”

  • Finally, put #import “GData.h” in your source code.

Happy coding 🙂

GData Static Library for Specific API