盤點一些iOS開發技巧:Swift和Objective-C
對蘋果開發者來講,2014年是令人難以置信的一年。在這短短的一年中發生瞭如此多的變化:在充滿吸引力的Swift面前,我們幾乎忘了之前是如何癡迷OC;以及充滿想象力的iOS8和WatchKit,難以想象還有什麼API能與之相比。
回顧過去一年發生在我們身邊的事情時,有一點不得不提:對蘋果開發者來講,2014年是令人難以置信的一年。在這短短的一年中(有關APP的開發)發生瞭如此多的變化:在充滿吸引力的Swift面前,我們幾乎忘了之前是如何癡迷於Objective-C;以及充滿想象力的iOS 8和WatchKit,難以想象還有什麼API能與之相比。
NSHipster的慣例:請可愛的童鞋們,在新年的第一天,爲大家展示你們(在開發中)常使用的技巧和方法。如今,隨着來自Cupertino(蘋果總部,位於舊金山)和衆多開源社區的一系列API的湧現,媽媽再也不用擔心我們找不到有趣的東西來分享啦!
在此,感謝以下童鞋們所做的貢獻:
Colin Rofls、Cédric Luthi、Florent Pillet、Heath Borders、Joe Zobkiw、Jon Friskics、Justin Miller、Marcin Matczuk、Mikael Konradsson、Nolan O'Brien、Robert Widmann、Sachin Palewar、Samuel Defago、Sebastian Wittenkamp、Vadim Shpakovski、Zak。
成員函數的使用技巧(來自Robert Widmann)
在用靜態方式調用Swift類和結構中的成員函數時,通常使用以下格式:
Object->(參數)->Things
比如,你可以用以下兩種方式調用reverse():
- [1,2,3,4].reverse( )
- Array.reverse([1,2,3,4])
[1,2,3,4].reverse( )
Array.reverse([1,2,3,4])
用@()來封裝C字符串(來自Samuel Defago)
事實上文字大部分時候是數字和字母的集合,使用C字符串,尤其當我在使用運行時編碼的時候,我常常會忘記用UTF8編碼、以NULL結束:Objective-C字符串封裝:
- NSString *propertyAttributesString =
- @(property_getAttributes(class_getProperty([NSObject class], "description")));
- // T@"NSString",R,C
NSString *propertyAttributesString =
@(property_getAttributes(class_getProperty([NSObject class], "description")));
// T@"NSString",R,C
AmIBeingDebuggedNolan O'Brien在這篇Q&A技術文檔中讓我們注意到了AmIBeingDebugged函數方法:
- #include <assert.h>
- #include <stdbool.h>
- #include <sys/types.h>
- #include <unistd.h>
- #include <sys/sysctl.h>
- static Bool AmIBeingDebugged(void) {
- int mib[4];
- struct kinfo_proc info;
- size_t size = sizeof(info);
- info.kp_proc.p_flag = 0;
- mib[0] = CTL_KERN;
- mib[1] = KERN_PROC;
- mib[2] = KERN_PROC_PID;
- mib[3] = getpid();
- sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);
- return (info.kp_proc.p_flag & P_TRACED) != 0;
- }
#include <assert.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sysctl.h>
static Bool AmIBeingDebugged(void) {
int mib[4];
struct kinfo_proc info;
size_t size = sizeof(info);
info.kp_proc.p_flag = 0;
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = getpid();
sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);
return (info.kp_proc.p_flag & P_TRACED) != 0;
}
使用延遲存儲屬性(來自Colin Rofls)
在開發過程中,應該避免使用Optionals類型,更不應該使用隱式解包optionals類型。你想聲明一個var變量卻不想給一個初始值?使用“lazy”吧,唯一要注意的就是:在你的屬性被賦值之前不要調用getter方法即可(童叟無欺!)
- lazy var someModelStructure = ExpensiveClass()
lazy var someModelStructure = ExpensiveClass()
假如你僅僅對這var變量調用set方法,而沒有調用getter方法的話,這個被lazy修飾的var變量不會被賦值。例如,用lazy修飾那些直到viewDidLoad時才需要初始化的views變量就會非常合適。獲取Storyboard視圖容器裏的子視圖控制器(來自Vadim Shpakovski)
有一個比較方便的方法來獲取故事板視圖容器裏的子視圖控制器:
- // 1. A property has the same name as a segue identifier in XIB
- @property (nonatomic) ChildViewController1 *childController1;
- @property (nonatomic) ChildViewController2 *childController2;
- // #pragma mark - UIViewController
- - (void)prepareForSegue:(UIStoryboardSegue *)segue
- sender:(id)sender
- {
- [super prepareForSegue:segue sender:sender];
- // 2. All known destination controllers assigned to properties
- if ([self respondsToSelector:NSSelectorFromString(segue.identifier)]) {
- [self setValue:segue.destinationViewController forKey:segue.identifier];
- }
- }
- - (void)viewDidLoad {
- [super viewDidLoad];
- // 3. Controllers already available bc viewDidLoad is called after prepareForSegue
- self.childController1.view.backgroundColor = [UIColor redColor];
- self.childController2.view.backgroundColor = [UIColor blueColor];
- }
// 1. A property has the same name as a segue identifier in XIB
@property (nonatomic) ChildViewController1 *childController1;
@property (nonatomic) ChildViewController2 *childController2;
// #pragma mark - UIViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue
sender:(id)sender
{
[super prepareForSegue:segue sender:sender];
// 2. All known destination controllers assigned to properties
if ([self respondsToSelector:NSSelectorFromString(segue.identifier)]) {
[self setValue:segue.destinationViewController forKey:segue.identifier];
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// 3. Controllers already available bc viewDidLoad is called after prepareForSegue
self.childController1.view.backgroundColor = [UIColor redColor];
self.childController2.view.backgroundColor = [UIColor blueColor];
}
重複運行項目,不重複構建項目(來自Heath Borders)
假如你一直在不停地調試同一個問題,你可以在不重複構建的情況下運行你的APP,這樣:“Product>Perform Action>Run without Building”
快速獲取Playground資源(來自Jon Friskics)
Swift裏的所有Playground共享相同的數據目錄:/Users/HOME/Documents/Shared Playground Data
如果你喜歡使用很多Playgrounds,你將需要在上述共享目錄下爲每個Playground新建對應的子目錄,來存儲每個Playground用到的數據;但是那之後你需要告訴每個Playground在哪兒可以獲取其對應的數據。下面是我常用的一個輔助解決方法:
- func pathToFileInSharedSubfolder(file: String) -> String {
- return XCPSharedDataDirectoryPath + "/" + NSProcessInfo.processInfo().processName + "/" + file
- }
func pathToFileInSharedSubfolder(file: String) -> String {
return XCPSharedDataDirectoryPath + "/" + NSProcessInfo.processInfo().processName + "/" + file
}
processName屬性是Playground文件的名字,因此只要你已經在Playground數據共享文件目錄下以相同的名字新建了一個子目錄,那麼你可以很容易訪問這些數據,和讀取本地JSON數據一樣:
- var jsonReadError:NSError?
- let jsonData = NSFileManager.defaultManager().contentsAtPath(pathToFileInSharedSubfolder("data.json"))!
- let jsonArray = NSJSONSerialization.JSONObjectWithData(jsonData, options: nil, error: &jsonReadError) as [AnyObject]
var jsonReadError:NSError?
let jsonData = NSFileManager.defaultManager().contentsAtPath(pathToFileInSharedSubfolder("data.json"))!
let jsonArray = NSJSONSerialization.JSONObjectWithData(jsonData, options: nil, error: &jsonReadError) as [AnyObject]
....或者訪問本地圖片
- let imageView = UIImageView()
- imageView.image = UIImage(contentsOfFile: pathToFileInSharedSubfolder("image.png"))
let imageView = UIImageView()
imageView.image = UIImage(contentsOfFile: pathToFileInSharedSubfolder("image.png"))
Please attention!本篇文章剩餘的部分來自Cédric Luthi大神的貢獻,他分享了一些比較有用的開發技巧和技術,這些內容足夠自成一篇,值得細細品讀。這裏再次感謝Cédric!
CocoaPods大揭祕
這兒有一個快速的方法來檢查APP裏用到的所有pods:
- $ class-dump -C Pods_ /Applications/Squire.app | grep -o "Pods_\w+"
$ class-dump -C Pods_ /Applications/Squire.app | grep -o "Pods_\w+"
CREATE_INFOPLIST_SECTION_IN_BINARY
注意Xcode中爲命令模式APP(command-line apps)設置的CREATE_INFOLIST_SECTION_IN_BINARY屬性。這比使用-sectcreate__TEXT__info_plist鏈接標誌位更加容易,前者還把已經編譯好的Info.plist文件嵌入在二進制編碼中。
關於如何向蘋果提需求,它也給我們上了一課,這個特性需求早在2006年的 rdar://4722772 被提出,但直到7年後才被滿足。
(譯者注:言外之意是它是反面教材,應該更有技巧的提需求)
禁用dylib鉤子(來自Sam Marshall)
Sam Marshall這個技巧可謂是走自己的路,讓黑客無路可走。
在你的“Other Linker Flags”里加上下面這行:
- -Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
NSBundle -preferredLocalizations
某些時候,你需要知道APP當前使用的是什麼語言。通常,大家會使用NSLocal+preferredLanguages. 可惜的是這個方法不會告訴你APP實際呈現的文字語種。你僅僅會得到iOS系統裏“Settings->General->Language&Region->Preferred Language”列表中的選項,或者OSX系統裏“System Preferences->Language & Region->Preferred Languages”列表中的選項。想象一下:優先語言列表中只有{英語,法語},但你的APP僅使用德語;調用[[NSLocal preferredLanguages] firstObject]返回給你的是英語,而不是德語。
正確的方法是用[[NSBundle mainBundle] preferredLocalizations]方法。
蘋果的開發文檔是這樣說的:
一個包含了在bundle中本地化的語言ID的NSString對象的數組,裏面的字符串排序是根據用戶的語言偏好設置和可使用的地理位置而來的。
NSBundle.h裏的備註:
一個bundle中本地化的子集,重新排序到當前執行壞境的優先序列裏,main bundle的語言順序中最前面的是用戶希望在UI界面上看到的語種。
當然你也許需要調用這個方法:
- NSLocal+canonicalLanguageIdentifierFromString:
NSLocal+canonicalLanguageIdentifierFromString:
來確保你使用的文字語種是規範的語種。
保護SDK頭文件
如果你用dmg安裝Xcode,那麼看看這篇Joar Wingfors的文章,它講述瞭如何通過保留所有權來避免SDK頭文件被意外修改:
- $ sudo ditto /Volumes/Xcode/Xcode.app /Applications/Xcode.app
$ sudo ditto /Volumes/Xcode/Xcode.app /Applications/Xcode.app
任意類型的實例變量檢測
爲了達到逆向處理的目的,查詢對象的實例變量是一個常見可靠的途徑。通常調用對象valueForKey:方法就能達到這一目的,除了少數重寫了類方法+accessInstanceVariablesDirectly的類屏蔽了該操作。
下面是一個例子:當實例變量有一個爲任意類型的屬性時,上述提到的操作無效
這是iOS6.1 SDK中MediaPlayer 框架的一段引用:
- @interface MPMoviePlayerController : NSObject {
- void *_internal; // 4 = 0x4
- BOOL _readyForDisplay; // 8 = 0x8
- }
@interface MPMoviePlayerController : NSObject {
void *_internal; // 4 = 0x4
BOOL _readyForDisplay; // 8 = 0x8
}
因爲 id internal=[moviePlayerController valueForKey:@”internal”] 無效,下面有一個笨辦法來取得這個變量:
- id internal = *((const id*)(void*)((uintptr_t)moviePlayerController + sizeof(Class)));
id internal = *((const id*)(void*)((uintptr_t)moviePlayerController + sizeof(Class)));
注意!不要隨意調用這段代碼,因爲ivar的佈局可能改變(指針偏移量計算可能出錯)。僅在逆向工程中使用!
NSDateFormatter +dateFormatFromTemplate:options:locale:
友情提示:假如你調用[NSDateFormatter setDateFormat],而沒有調用[NSDateFormatter dateFormatFromTemplate:options:local:],n那麼很可能出錯。
蘋果文檔:
- + (NSString *)dateFormatFromTemplate:(NSString *)template
- options:(NSUInteger)opts
- locale:(NSLocale *)locale
+ (NSString *)dateFormatFromTemplate:(NSString *)template
options:(NSUInteger)opts
locale:(NSLocale *)locale
不同地區有不同的日期格式。使用這個方法的目的:得到指定地區指定日期字段的一個合適的格式(通常你可以通過currentLocal查看當前所屬地區)
下面這個例子給我們表現了英式英語和美式英語不同的日期格式:
- NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
- NSLocale *gbLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
- NSString *dateFormat;
- NSString *dateComponents = @"yMMMMd";
- dateFormat = [NSDateFormatter dateFormatFromTemplate:dateComponents options:0 locale:usLocale];
- NSLog(@"Date format for %@: %@",
- [usLocale displayNameForKey:NSLocaleIdentifier value:[usLocale localeIdentifier]], dateFormat);
- dateFormat = [NSDateFormatter dateFormatFromTemplate:dateComponents options:0 locale:gbLocale];
- NSLog(@"Date format for %@: %@",
- [gbLocale displayNameForKey:NSLocaleIdentifier value:[gbLocale localeIdentifier]], dateFormat);
- // Output:
- // Date format for English (United States): MMMM d, y
- // Date format for English (United Kingdom): d MMMM y
NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
NSLocale *gbLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
NSString *dateFormat;
NSString *dateComponents = @"yMMMMd";
dateFormat = [NSDateFormatter dateFormatFromTemplate:dateComponents options:0 locale:usLocale];
NSLog(@"Date format for %@: %@",
[usLocale displayNameForKey:NSLocaleIdentifier value:[usLocale localeIdentifier]], dateFormat);
dateFormat = [NSDateFormatter dateFormatFromTemplate:dateComponents options:0 locale:gbLocale];
NSLog(@"Date format for %@: %@",
[gbLocale displayNameForKey:NSLocaleIdentifier value:[gbLocale localeIdentifier]], dateFormat);
// Output:
// Date format for English (United States): MMMM d, y
// Date format for English (United Kingdom): d MMMM y
通過調試獲取內部常量
近期,Matthias Tretter在Twitter上問到:
有人知道在iOS8裏modal viewController presentation的默認動畫時間和跳轉方式嗎?
我們在UIKit的類庫中發現了這樣一個函數:[UITransitionView defaultDurationForTransition:],並在這個方法的位置加一個斷點:
- (lldb) br set -n "+[UITransitionView defaultDurationForTransition:]"
(lldb) br set -n "+[UITransitionView defaultDurationForTransition:]"
模態顯示一個viewController,就會停在這個斷點,輸入finish執行該方法:- (lldb)finish
(lldb)finish
在defaultDurationForTransition:被執行時,你就能讀到結果(在xmm0寄存器裏)- (lldb) register read xmm0 --format float64
- xmm0 = {0.4 0}
(lldb) register read xmm0 --format float64
xmm0 = {0.4 0}
回覆:默認動畫時間0.4sDIY 弱關聯對象
不幸的是,關聯對象OBJC_ASSOCIATION_ASSIGN策略不支持引用計數爲0的弱引用。幸運的是,你可以很容易實現它,你僅僅需要一個簡單的類,並在這個類裏弱引用一個對象:
- @interface WeakObjectContainter : NSObject
- @property (nonatomic, readonly, weak) id object;
- @end
- @implementation WeakObjectContainter
- - (instancetype)initWithObject:(id)object {
- self = [super init];
- if (!self) {
- return nil;
- }
- self.object = object;
- return self;
- }
- @end
@interface WeakObjectContainter : NSObject
@property (nonatomic, readonly, weak) id object;
@end
@implementation WeakObjectContainter
- (instancetype)initWithObject:(id)object {
self = [super init];
if (!self) {
return nil;
}
self.object = object;
return self;
}
@end
然後,通過OBJC_ASSOCIATION_RETAIN(_NONATOMIC)關聯WeakObjectContainter:- objc_setAssociatedObject(self, &MyKey, [[WeakObjectContainter alloc] initWithObject:object], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, &MyKey, [[WeakObjectContainter alloc] initWithObject:object], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
用object屬性指向這個所需的引用計數爲0的弱引用對象。- id object = [objc_getAssociatedObject(self, &MyKey) object];