'IOS/공통'에 해당되는 글 12건

  1. 2017.08.31 Extension
  2. 2017.06.11 Bundle Programming Guide
  3. 2017.05.03 Event Handling Guide - Responders
  4. 2017.04.07 Event Handling Guide - Gesture
  5. 2017.03.26 Objective-C, Swift 기초
  6. 2017.02.19 Cocoa Pods
  7. 2017.02.14 About Bundle
  8. 2016.12.18 iOS Architecture
  9. 2016.02.18 UIApplicationMain?
  10. 2015.11.05 디버깅 (Crash 핸들링) 1

Extension

IOS/공통 2017. 8. 31. 15:31

Introduction
: Extension은 앱과 다르다. Extension의 바이너리는 Extension포함 또는 배포하는 앱과는 별개로 실행된다. 


An App Extension's Life Cycle
: extension은 App이 아니기 때문에, App의 Life cycle과 다르다. 대부분의 경우 사용자가 사용중인 앱에서 Extension을 사용하려고 할때 Extension이 Launch 된다.
즉, App 에서 실행될 Extension에 request를 보내면 Extension이 Launch, Extension code가 실행되고 해당동작이 끝나면 시스템이 extension을 kill한다.


How an App Extension Communicates
: Host app(extension을 사용하는 앱)이 Extension에게 Request를 날리면, Extension이 해당 처리를 하고 Host app에게 response를 날리는 형식이다. 

<Containing app - Extension을 소유하고 있는 앱> 

containing app과 host app은 전혀 communication 하지 않는다. 심지어 대부분의 경우 extension이 실행 될때 containing app이 실행되지 않는다.

다만, Today widget과 같은 몇몇 extension은 NSExtensionContext의 openURL:completionHandler: 메서드를 통해 containing app을 열 수 있다. 그리고 containing app은 필요한 경우 app extension과 shared resource가 저장된 공통의 private한 저장소를 공유할 수 있다. 아래 그림을 보자.



Some APIs Are Unavailable to App Extensions
: 몇몇 API들은 extension에서 사용할 수 없다.

- sharedApplication 객체를 아예 사용 못함
- API의 헤더파일에 "NS_EXTENSION_UNAVAILABLE" 또는 비슷한 의미의 매크로가 붙은 것은 사용 불가. (ex- in iOS 8에서 HealthKit Framework, EventKit UI Framework)
- camera, microphone 을 접근할 수 없다. (다만, iMessage app은 camera, microphone을 사용할 수 있다. 그 대신 Info.plist에 NSCameraUsageDescription, NSMicrophoneUsageDescription을 세팅해야 한다.)
- 장시간 걸리는 background task는 수행할 수 없다. (limit 정책은 플랫폼에 따라 다양하다. 나중에 언급하도록 하겠다. 그리고 extension은 NSURLSession을 통해 upload, download 작업을 할 수 있다.)
- AirDrop을 통해서 data를 받을 수 없다. (다만, UIActivityViewController class를 통해서 AirDrop으로 data를 보내는 것은 가능하다.)
 UIBackgroundModes를 extension의 plist에 포함하면 reject 당한다.


Using an Embedded Framework to Share Code
: containing app 과 extension이 코드를 공유하기를 원한다면 embed framework에 해당 코드를 넣고 embed framework를 양쪽 타겟에 포함시키면 된다.
단, embed framework에는 extension에서 사용할 수 없는 API들을 포함 시키면 안된다. 
그리고 extension target의 "Require Only App-Extension-Safe API"를 YES로 세팅해야 한다. 
또한, build phase 탭의 Embed Frameworks의 Destination을 "Frameworks"로 지정해야 한다. "SharedFramework"로 지정할 경우 앱 심사에서 reject 당한다. 


Sharing Data with Your Containing App
: app extension과 containing app은 기본적으로 서로의 container에 접근하지 못한다. 그러나 서로의 데이터를 공유하기 위한 방법이 있다.

- xCode 또는 Developer portal에서 app groups을 enable 시킨다.
- appGroup을 등록하고, containing app이 appGroup을 사용하도록 지정한다.
- 위 두단계를 거치면, containing app과 extension은 NSUserDefaults를 사용하여 data를 공유할 수 있다. UserDefaults를 init할때 shared group의 identifier로 init 해야 한다. 

NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.example.domain.MyShareExtension"];

[mySharedDefaults setObject:theAccountName forKey:@"lastAccountName"];

* extension에서 NSURLSession을 사용하여 background upload, download를 하려면 shared container를 setup 해야한다. 그래야만 extension과 containing app 모두가 전송되는 data에 접근할 수 있다. (https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW2)

*Version Note
in IOS 8.2 and later, you can alternatively employ the UIDocument class to coordinate shared data access.

in IOS 9 and later, you can employ the NSFileCoordinator class directly for shared data access, but if you do this you must remove your NSFilePresenter objects when your app extension transitions into the background.


Accessing a Webpage
: Share extension, action extension에서 javaScript 파일을 통해서 web contents 들을 extension 으로 받아올 수 있다. 예를 들면, Share extension을 통해서 web content를 자신의 앱에 공유하는 기능을 추가할 수 있다는 것이다.(https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1)


Declaring Supported Data Types for a Share or Action Extension
: Share 또는 Action extension에서 host app이 extension에 data를 넘겨주는데, extension 에서 넘겨받기를 원하는 data type을 지정할 수 있다. extension의 Info.plist의 NSExtensionActivationRule key로 지정한다. 또한 해당 타입 data의 max, min 갯수도 지정할 수 있다. 

  1. <key>NSExtensionAttributes</key>
  2. <dict>
  3. <key>NSExtensionActivationRule</key>
  4. <dict>
  5. <key>NSExtensionActivationSupportsImageWithMaxCount</key>
  6. <integer>10</integer>
  7. <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
  8. <integer>1</integer>
  9. <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
  10. <integer>1</integer>
  11. </dict>
  12. </dict>
  13. 특정 data type을 지원하지 않으려면 0값을 세팅하거나 해당 데이터 타입의 key, value
  14. 를 삭제하면 된다.

* Share 또는 Action Extension에서 webpage에 접근하기를 원한다면 NSExtensionActivationSupportsWebPageWithMaxCount key의 값을 지정하면 된다.

위의 NSExtensionActivationRule dictionary에 값을 지정하는 것만으로도 충분히 타입지원을 할 수 있지만, 좀 더 세밀하게 컨트롤하고 싶을 경우 "predicate statement"(NSPredicate) 문법을 통해 지원할 수 있다.(https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1)



Deploying a Containing App to Older Versions of iOS
- https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1(맨 아래 참고)


* 각 extension type에 대한 설명
- https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/Action.html#//apple_ref/doc/uid/TP40014214-CH13-SW1

'IOS > 공통' 카테고리의 다른 글

Bundle Programming Guide  (0) 2017.06.11
Event Handling Guide - Responders  (0) 2017.05.03
Event Handling Guide - Gesture  (0) 2017.04.07
Objective-C, Swift 기초  (0) 2017.03.26
Cocoa Pods  (0) 2017.02.19
Posted by 홍성곤
,

Application Bundles
: Application Bundle은 앱이 실행되기 위한 모든 데이터들을 담고 있는 bundle이라고 보면 된다. 상세 구조는 개발하는 Platform에 따라 달라질 수 있지만 사용하는 방법은 똑같다.

What Files Go Into an Application Bundle?
1) Info.Plist(필수) 
-  앱의 설정 파일이라고 보면 된다.
2) Executable(필수)
- 앱은 반드시 이 파일을 포함해야 한다. 이 파일은 앱의 main entry point와 app target으로 링크된 code가 포함되어 있다.
3) Resource files
- 이 파일은 executable file 바깥에 있다. 대표적으로 image, icon, sound, nib file, string file, configuration file, data file등이 포함된다. 대부분의 resource file들은 사용언어, 지역에 따라 localized될 수 있다. 
- Resource는 localized가 필요한것과 그렇지 않은것으로 나뉠수 있는데, localized가 필요한것을 app 번들 root에 각자 subdirectory들이 존재하고 그 subdirectory안에 똑같은 파일들과 똑같은 이름을 가진 파일들이 각각 들어가 있다. app이 localized resource를 찾을때는 language preference를 참조해서 해당 폴더를 결정한다.
4) Other support files
- custom data resource를 Application bundle에 포함 시킬 수 있다. 하지만 custom framework나 plug-in은 포함하지 못한다.

Anatomy of an iOS Application Bundle
The iOS Application bundle Structure
: 전형적인 iOS Application Bundle은 App이 사용하는 Executable과 resource들을 bundle top directory안에 포함한다.

*Bundle structure of an iOS application

- subdirectory에 있는 파일들은 localized가 필요한 파일들이다. 또한 개발자들이 필요에 의해서 추가적인 subdirectory들도 만들수 있다. 

*Contents of a typical iOS application bundle 
1) MyApp(필수)
- executable file이다. application 코드가 포함되어 있다. 파일 이름은 application name에 ".app" extension을 제외한 문자열이다.
2) Application icons(필수/권고)
- MyAppIcon.png, MySearchIcon.png, MySettingIcon.png
- 모든 icon이 필수인것은 아니지만 모두 포함하기를 권고하고 있다. SearchIcon은 앱 검색시 나오는 아이콘이고 MySettingIcon은 설정화면에서 노출되는 아이콘 이미지 이다.
3) Info.plist(필수)
- 앱 정보, 설정파일
- bundle ID, version number, display name.
4) Launch images(권고)
- 앱이 처음 켜질때 보여지는 하나 또는 그 이상의 이미지들이다.
5) MainWindow.nib(권고)
- 앱의 main nib file이다. 대표적으로, 이 파일은 app의 main window object, application delegate object를 포함한다.
- 요즘도 널리 사용중인지는 모르겠다.(본적이 없음..)
6) Setting.bundle
- plug-in의 특별한 타입이라고 보면 된다. 설정 앱에 추가하고 싶은 컨텐츠를 설정하고 싶을 때 사용한다. 
- property list, other resource file들을 포함한다.
7) Custom resource files
- nib file, image, sound files, configuration files, string files, any other custom data files.
- Non-localized resource들은 top level directory에 위치하고, localized resource들은 language-specific subdirectory 안에 위치한다.

* iOS app bundle은 "Resources"라는 이름의 custom folder를 포함하지 못한다.



Framework Bundles
: Framework는 dynamic shared library와 resource file을 포함하는 directory구조다.
Framework는 dynamic shared library가 가지는 장점을 그대로 가진다. 즉, 여러 application에서 같은 dynamic shared library를 사용하면 메모리에는 하나의 library만 올라간다. 모든 application에서 같은 library라 할지라도 각각의 library와 resource file을 중복으로 가지고 있거나 사용하는 비효율을 막기 위함이다. 
동작 방식은 build 타임에는 shared library를 링킹만 시켜주고 application이 실제 로드 될때 dynamic loader가 링크된 shared library를 참조해서 이미 로딩되어 있으면 기존것을 사용하고 로딩되어 있지 않으면 shared library를 로드해서 사용하는 방식이다.

* 오직 code와 read only resource들만 공유한다. writable resource들은 각자 application이 copy해서 사용한다.

Anatomy of a Framework Bundle
: Framework는 dynamic shared library를 포함하기 때문에 다른 앱이 같은 framework를 쓰지만 framework의 version이 서로 다를 경우가 있다. 이를 위해서 Framework의 구조는 multiple version을 지원하는 versioned bundle구조로 되어 있다. 
즉, multiple version의 code와 resource들을 포함할 수 있다. 필요에 따라서 하위 버전을 사용하는 앱이 최신버전의 framework를 사용하도록 업데이트 되는것도 허용된다.

아래는 framework의 구조이고, symbolic link가 어떤식으로 되어있는지 보여준다.






Localized Resources in Bundles
: localized subdirectory의 이름은 'language_region.lproj'의 규칙을 따라야 한다.
language 부분은 두글자를 써서 구분한다.(ISO 639 convention을 따름.) 
region 부분 역시 두글자를 써서 구분한다.(ISO 3166 convention을 따름.) region을 명시하는 것은 optional 이지만 지역별로 다르게 대응해줘야 할 경우 유용하게 사용된다.
예를들어 'en.lproj'로 영어 국가는 모두 대응할 수 있다. 하지만 'en_GB.lproj', 'en_AU.lproj', 'en_US.lproj'를 통해 영국, 호주, 미국을 따로 대응할 수 있다.

*NSBundle, CFNundleRef는 이전버전과의 호환성을 위해 human-readable directory names응 지원한다. English.lproj, japanese.lproj 등등.. 
하지만 ISO convention을 따르는 것을 추천한다.

loader는 정해진 순서대로 해당 폴더를 참조하여 resource를 가져온다. 처음에는 언어+지역에 해당하는 폴더에서 resource를 찾고 그곳에 없으면 언어에 해당하는 폴더에서 찾고 그곳마저 없으면 non-localized resource들 중에 찾는다. 즉, 모든 localized subdirectory안에 resource 파일을 폴더마다 중복으로 가지고 있을 필요가 없다.




이와같이 언어_지역 subdirectory는 localized resource를 full set으로 가지고 있을 필요가 없다. 없는 resource는 언어 subdirectory에도 찾을 것이기 때문이다. 하지만 언어 subdirectory는 localized resource를 full set으로 가지고 있어야 한다.

*Resource 폴더는 무시해라.(Mac app에만 존재하는 구조이다. iOS는 App bundle root라고 생각하면 된다.)






'IOS > 공통' 카테고리의 다른 글

Extension  (0) 2017.08.31
Event Handling Guide - Responders  (0) 2017.05.03
Event Handling Guide - Gesture  (0) 2017.04.07
Objective-C, Swift 기초  (0) 2017.03.26
Cocoa Pods  (0) 2017.02.19
Posted by 홍성곤
,

Understanding Responders and the Responder Chain
: UIKit은 event가 들어오면 적절한 responder object로 전달한다. responder object는 UIResponder 또는 이를 상속한 subclass들이다. UIView, UIViewController, UIApplication이 이에 해당한다. 
제일 처음 event를 전달받는 object를 first-responder라 부른다. first-responder가 event를 처리하지 못하면 event는 responder chain에 의해서 next-responder로 넘어간다. 해당 flow를 그림을 통해 살펴보자.

위 그림에서 사용자가 UITextField를 클릭했다고 가정 시, first-responder인 UITextField가 event를 처리할 것이다. 만약 UITextField가 event를 처리하지 못하면 superview에게 event가 넘어간다. 이렇게 superview를 계속 따라가다보면 root view가 나오는데 root view 마저 event를 처리하지 못하면 UIViewController가 event를 처리하게 된다. 만약 UIViewController도 event를 처리하지 못하면 자신을 present하고 있는 viewController가 있으면 그쪽으로 event를 넘기고 아닐경우 UIWindow가 event를 받고, 역시 처리하지 못할 경우에는 UIApplication이 받아서 event를 UIApplicationDelegate로 넘긴다.

위 로직을 간단히 정리해 보면 다음과 같다.
first-responder -> super view -> UIViewController -> presenting ViewController -> UIWindow -> UIApplication(UIApplicationDelegate) 


* 만약, view에 gesture recognizer가 설정되어 있으면 touch event가 무시될 수 있다. 예를 들어 view에  tap gesture recognizer가 설정되어 있고, UIGestureRecognizer의 delaysTouchesBegan이 YES로 설정되어 있으면 해당 view를 tap해도 tap gesture action만 불리고 touchesBegan:withEvent: 메서드를 불리지 않는다. 
이러한 제어들은 UIGestureRecognizer의 delaysTouchesBegan, delaysTouchesEnded, cancelsTouchesInView를 통해 할 수 있다.

Determining Which Responder Contained a Touch Event
: UIKit은 어느 view에서 touch가 일어났는지 판별하기 위해서 UIView의 hitTest:withEvent: method를 사용한다. hitTest:withEvent: 메소드는 touch가 일어난 point를 포함하는 subview들 중 가장 먼 subview를 return한다. 바로 그 view가 first-responder가 되는것이다.


Altering The Responder Chain
: nextResponder 프로퍼티를 override해서 자신이 원하는 object를 nextResponder로 지정할 수 있다.
UIView는 이미 nextResponder를 override해서 사용하고 있다. 만약 UIView가 viewcontroller의 Root View라면, nextResponder를 viewcontroller로 지정하고 root view가 아니면 super view로 지정한다. 
UIViewController도 마찬가지로 자신의 root view가 UIWindow의 root view라면 UIWindow로 next responder를 지정하고 그게 아니면 presenting ViewController로 지정한다.

Directing Input to a Specific Responder
: UIResponder를 상속받은 object들은 event를 받으면 firstResponder가 될 수 있다. 다만, UITextFiled, UITextView처럼 event를 받는 즉시 자동으로 firstResponder가 되는 경우가 있고, UIButton, UIView처럼 명시적으로 becomeFirstResponder 메서드를 호출해야 firstResponder가 되는 경우가 있다. 
firstResponder가 되면 UIKit은 해당 object에 등록해논 input view를 display한다. UITextField, UITextView의 input view는 system keyboard view이다. 

Programmatically Changing the First Responder
: 명시적으로 firstResponder로 지정하기 위해 becomeFirstResponder 메서드를 호출하면 UIKit은 해당 object가 firstResponder가 될 수 있는지 몇가지 체크를 한다.

1. 현재의 firstResponder object에게 canResignFirstResponder 메시지를 보내 firstResponder를 resign할 수 있는지 확인한다.
보통 대부분의 object들은 YES를 리턴하지만, 필요에 따라 NO를 리턴하게 하는 경우가 있다. 대표적으로 textField, textView가 invalid한 data를 포함했을 경우이다.
2. firstResponder로 지정하고자 하는 object에게 canBecomeFirstResponder 메시지를 보내 firstResponder가 될 수 있는지 확인한다.(default값이 NO다.)

Assigning an Input View to a Responder
: object가 firstResponder가 될 때 inputView를 띄우기 위해서는 해당 object에 inputView 또는 inputViewController 프로퍼티를 세팅해줘야 한다. inputView, inputViewController를 올바르게 사용하기 위해서 알아둬야할 몇가지가 있다.

1. inputView의 UI를 screen width에 맞춰서 구현해야 한다.
- UIKit은 screen width와 내가 지정한 높이로 input view의 size를 결정하기 때문이다.
2. delegate를 사용해서 inputView를 통해 들어온 data를 firstResponder에게 전해줘라.
3. keyboard notifications를 사용해라.
- system keyboard view뿐만 아니라 firstResponder의 inputView는 모두 keyboard notification들을 사용할 수 있다.
(UIKeyboardWillChangeFrameNotification, UIKeyboardWillShowNotification, UIKeyboardDidChangeFrameNotification, UIKeyboardDidShowNotification)

responder는 inputView이외에도 accessory view를 가질 수 있다. inputAccessoryView, inputAccessoryViewController 프로퍼티를 이용해서 지정한다. accessory view는 input view를 보조하는 역할을 한다. 예를들어 system keyboard view는 accessory view를 이용해서 자동완성 기능을 제공한다. 

'IOS > 공통' 카테고리의 다른 글

Extension  (0) 2017.08.31
Bundle Programming Guide  (0) 2017.06.11
Event Handling Guide - Gesture  (0) 2017.04.07
Objective-C, Swift 기초  (0) 2017.03.26
Cocoa Pods  (0) 2017.02.19
Posted by 홍성곤
,

Gestures
Tap Gesture

tap gesture action 메서드가 호출되지 않을 때.
- userInteractionEnabled가 YES인지 확인
- numberOfTapsRequired 프로퍼티가 제대로 설정되었는지 확인.
- numberOfTouchesRequired 프로퍼티가 제대로 설정되었는지 확인

Long Press Gesture
long press gesture action 메서드가 호출되자 않을 때.
- userInteractionEnabled가 YES인지 확인.
- mininumPressDuration 프로퍼티에 설정된 시간보다 길게 press 하고 있었는지 체크.
- numberOfTapsRequired 프로퍼티가 제대로 설정되었는지 확인.
- numberOfTouchesRequired 프로퍼티가 제대로 설정되었는지 확인.

Pan Gesture
- UIPanGestureRecognizer 이외에 UIScreenEdgePanGestureRecognizer가 있다. UIScreenEdgePanGestureRecognizer는 스크린 Edge에서 panGesture가 발생할 경우 핸들링 하기 위한 것이다.

pan gesture action 메서드가 호출되지 않을 때.
- userInteractionEnabled가 YES인지 확인.
- mininumNumberOfTouches와 maximumNumberOfTouches사이 갯수의 손가락으로 터치해야 한다.
- UIScreenEdgePanGestureRecognizer의 경우 edges 프로퍼티가 설정되어 있어야 하고, 해당 edges에서 event가 시작되야 한다.

Swipe Gesture
swipe gesture action 메서드가 호출되지 않을 때.
- userInteractionEnabled가 YES인지 확인.
- touch한 손가락 갯수가 numberOfTouchesRequired와 같아야 한다. 
- direction 프로퍼티와 swipe한 방향이 맞아야 한다. 

*Pinch Gesture, Rotation Gesture는 생략.


Custom Gesture Recognizer
: Custom Gesture Recognizer를 구현하기 위해서는 UIGetstureRecognizerSubclass.h 파일을 import하고 touchesBegan:withEvent, touchesMoved:withEvent:, touchesEnded:withEvent, touchesCancelled:withEvent: 등의 메서드를 오버라이드 해서 custom recognizer를 구현해 줘야 한다. 
올바르게 구현하려면 discrete gesture recognizer, continuous gesture recognizer가 각각 가질수 있는 state등 여러가지 사전 지식이 필요하다. 자세한 구현방법은 설명하지 않겠다.

참조
https://developer.apple.com/library/content/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/ImplementingaCustomGestureRecognizer.html#//apple_ref/doc/uid/TP40009541-CH8-SW1


Coordinating Multiple Gesture Recognizers
: 동시에 두개 이상의 Gesture Recognizer를 다룰때, 별 다른 처리를 하지 않으면 충돌하기 마련이다. 예를 들어, 하나의 뷰에 Pen gesture Recognizer와 Swipe gesture Recognizer를 동시에 설정하면 항상 Pen gesture만 인식이 될 것이다. Pen gesture는 continuous gesture이기 때문에 Swipe gesture 보다 우선순위가 높다. 이런 경우에 우선순위를 어떻게 처리해야할 지 알아보겠다.

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
    if gestureRecognizer = self.penGesture && otherGestureRecognizer == self.swipeGesture
    {
        return true
    }
    
    return false
}
위 메서드는 UIGestureRecognizer의 delegate인데 panGesture에 해당 delegate를 설정해 주고 swipeGesture의 state가 fail이 아닌 경우에만 panGesture를 인식하도록 했다.

또 다른 delegate가 있는데 주체만 바뀌었을 뿐 동작 방식은 같다. 

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
    if gestureRecognizer = self. swipeGesture && otherGestureRecognizer == self.panGesture
    {
        return true
    }
    
    return false    
}

또한, 동시에 2개이상의 gesture recognition을 허용할 수 있다.
gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: delegate를 설정하여 처리하면 된다. 이 delegate에서 YES를 return 하면 Simultaneous Recognition을 허용하겠다는 것이다.


Attaching Gesture Recognizers to UIKit Controls
: 일반적으로 내가 설정한 gesture recognizer는 UIKit Control들이 자체적으로 가지고 있는 event handling 동작에 영향을 미치지 않는다. 예를 들어 view에 버튼을 올리고 view에  tapGesture를 등록해도 버튼 event에 대한 동작은 손상되지 않는다는 것이다. 다만 버튼 evnet에 대한 동작에 우선하는 gesture recognizer를 등록하고 싶다면 button 자체에 gesture recognizer를 등록해야 한다. 다만, 추천하지는 않는다. 


출처
- https://developer.apple.com/library/content/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/index.html#//apple_ref/doc/uid/TP40009541-CH3-SW1

'IOS > 공통' 카테고리의 다른 글

Bundle Programming Guide  (0) 2017.06.11
Event Handling Guide - Responders  (0) 2017.05.03
Objective-C, Swift 기초  (0) 2017.03.26
Cocoa Pods  (0) 2017.02.19
About Bundle  (0) 2017.02.14
Posted by 홍성곤
,

스위프트 중간 언어(SIL)
: 스위프트 컴파일러, swiftc는 스위프트 코드를 SIL(Swift Intermediate Language)로 만들고 LLVM IR로 변환한다. 또 IR 최적화기를 거쳐서 최종적으로 타깃머신에 맞는 기계코드를 생성한다.

swift 코드 -> SIL -> LLVM IR -> 기계 코드

Objective-C 호환성
: Objective-C에서 스위프트 객체를 사용하려면 NSObject를 최상위 클래스로 지정하고 상속받아 만들어야 한다. 그렇지 않으면 스위프트 전용 객체로 동작하기 때문에 Objective-C 에서는 사용할 수 없다. 
NSObject 객체를 상속받은 경우 스위프트 객체는 내부에서 Objective-C 객체로 자동 변환되고, Objective-C 런타임에서 동작한다.


Objective-C 객체와 메모리 구조
: 모든 운영체제는 동일한 구조로 메모리를 관리한다.
1- Stack: 함수, 함수 내에 변수에 대한 메모리 저장
2- Heap: 클래스, 클래스 인스턴스에 대한 메모리 동적 할당(클래스에 대한 메모리를 프로그램 실행시 로드된다.), Stack과 Heap 사이에 빈공간이 있음, Stack 윗 공간은 커널 영역이기 때문에 커널 영역을 보호하기 위해서 스택은 위에서 부터 메모리를 사용한고 Heap은 밑에서 부터 사용하기 때문에 두 메모리 공간을 많이 사용하면 할수록 빈 메모리 공간이 줄어든다.
3- BSS(심벌): 아직 초기화 되지않은 전역변수에 대한 메모리 저장
4- Data: 초기화가 완료된 전역변수에 대한 메모리 저장
5- Text: 프로그램 실행코드(실제 함수 구현, heap에 클래스에 대한 메모리가 저장되는데 여기에 클래스 메서드, 인스턴스 메서드의 Selector 변수들이 저장되는 것이고, 이 Selector에 대한 실제 구현 함수 코드는 Text영역에 할당된다.)


객체 예외성
: 모든 코코아 객체 인스턴스가 힙 영역에 생성되는 것은 아니다. 특이하게도 힙이 아니라 텍스트 영역과 데이터 영역에 생기는 경우가 있다. 
NSString 클래스는 NSObject에서 상속받는 코코아 클래스 중에서 유일하게 전역변수로 선언할 수 있다. NSString을 메서드 내에서 사용하더라도 NSString 지역변수는 Stack에 저장되고 NSString 클래스와 클래스 인스턴스에 대한 메모리는 Data영역에 저장되고 해당 문자 리터럴은 Text 영역에 저장된다. 즉 스택에 여러개의 NSString 변수가 생성되더라도 문자 리터럴이 같으면 중복으로 저장해서 사용할 필요가 없기 때문에 하나의 클래스, 클래스 인스턴스 메모리만 Data영역에 저장되고 문자 리터럴도 Text영역에 하나만 저장된다.

* Swift에서 String 객체는 네이티브 문자열 객체를 만들 수도있고, NSString을 연결해서 쓸수도 있다. 다만, 네이티브 문장열로 만들 경우에는 Objective-C의 NSString처럼 클래스, 클래스 인스턴스에 대한 메모리 할당 없이 Text영역에 있는 문자 리터럴을 포인터로 바로 연결해서 사용하기 때문에 NSString 보다는 가벼운 형태라고 할 수 있다. 


hash 메서드 
: isEqual: 메서드를 오버라이드한 경우라면 반드시 -hash 메서드도 다시 구현해야 한다. 왜냐하면 NSDictionary 같은 컬렉션 객체는 -isEqual: 메서드 대신 -hash 메서드를 사용해서 해시 값을 비교하기 때문이다. NSObject에 기본적으로 구현된 -hash 메서드는 self 메모리 포인터 값을 NSUInteger 타입 숫자로 바꿔줄 뿐이다. 따라서 -hash 메서드를 구현할 때도 -isEqual 메서드를 구현할 때와 마찬가지로 모든 속성에 대해 -hash 메서드를 호출해서 해시 값을 구하면 된다.


인스턴스, 클래스, 메타 클래스
: 인스턴스, 클래스, 메타 클래스 모두 isa 포인터를 가지고 있다. 인스턴스의 isa 포인터는 클래스를 가리키고 클래스의 isa 포인터는 메타 클래스를 가리키고 있다. 마지막으로 메타 클래스의 isa 포인터는 슈퍼메타 클래스가 있으면 그것을 가리키고 자신이 root 메타 클래스이면 자기 자신을 가리킨다. 인스턴스는 멤버 변수에 대한 정보만 가지고 있고, 클래스는 인스턴스 메서드 목록과 코드를 가지고 있으며, 메타 클래스는 클래스 메서드 목록과 코드를 가지고 있다. 


ARC 규칙
1) C 구조체 내부에 객체 포인터를 넣지마라
: C언어에서 사용하는  struct나 union 내부에 Objective-C 객체 포인터를 넣으면 ARC에서 메모리 관리를 할 수 없기 때문에 컴파일 에러가 발생한다.
ARC가 관리하지 않는다는 것을 강제로 명시하려면 '__unsafe_unretained' 수식어를 사용해야 한다. 다만, 해당 수식어를 사용하면 dangling 포인터에 접근할 위험이 있다.

2) id와 void* 타입을 명시적으로 타입 변환하라 


타입 연결(Objective-C 객체 <-> Core Foundation 객체)
: Core Foundation 객체란, C로 만들어진 구조체를 말한다.
대부분의 Core Foundation 구조체는 대응되는 Objective-C 객체와 toll-free bridged 된다. 즉, 무비용으로 타입 변환이 되는것이다. (CFRunLoop <-> NSRunLoop, CFBundle <-> NSBundle 등은 제외)
Core Foundation 구조체는 ARC가 메모리 관리를 하지 못하기 때문에 프로그래머가 직접 메모리 관리를 해줘야 한다. 때문에, Objective-C 객체로 타입 변환할 때에도 적절한 메모리 관리 코드가 삽입되야 한다. 
이제 부터 여러가지 타입변환 방법에 대해 알아보겠다.

1) __bridge 방식
: 객체의 소유권을 넘기지 않고 타입 연결만 하는 경우에 사용한다. 다만, 허상 포인터가 생길 수 있기 때문에 매우 위험하다. 

id sObject = [[NSObject alloc] init];
void *pObject = (__bridge void*) aObject;
id sOtherObject = (__bridge id) pObject;

2) __bridge_retained 또는 CFBridgingRetain 방식
: Objective-C 객체를 Core Foundation 포인터로 연결하면서 소유권도 주는 경우에 사용한다. 참조가 끝나면 CFRelease()와 같은 함수를 이용해서 소유권을 반환해야 한다.

id sObject = [[NSObject alloc] init];
void *pObject = (__bridge_retained void*) sObject;
// (__bridge_retained void*) 대신 CFBridgingRetain() 매크로를 사용해도 된다.

위 코드는 컴파일러에 의해서 다음과 같이 변환된다.

id sObject = [[NSObject alloc] init];
void *pObject = aObject;
[(id)pObject retain];

3) __bridging_transfer 또는 CGBridgingRelease 방식
: __bridge_retained와는 반대로 Core Foundation 참조 포인터를 Objective-C 객체로 연결하면서 소유권을 넘기는 경우에 사용한다.

id sObject = (__bridge_transfer id)pObject;
// (__bridge_transfer id) 대신 CFBridgingRelease() 매크로를 사용해도 된다.

위 코드는 컴파일러에 의해서 다음과 같이 변환된다.

is sObject = (id)pObject;
[sObject retain]:
[(id)aObject release];

Collection 클래스 성능
- Collection 클래스들은 직접 for문을 돌면서 objectAtIndex:, objectForKey: 등을 통해 iterating 하는것 보다 'for in' (fast enumeration), enumerateKeysAndObjectsUsingBlock: 등을 사용해서 iterating을 할때 더욱 성능이 좋다. 그 이유는 objectAtIndex, objectForKey 같은 메서드들은 해당 index, key에 대해 매칭되는 값을 찾아야 되고 해당 index, key에 대한 int, string 값을 생성하는 시간도 필요하기 때문이다.
'for in' 이나 enumerateKeysAndObjectsUsingBlock: 같은 경우는 이러한 것들을 고려하지 않고 단순히 iterating에만 신경 쓰면 된다.


NSArray vs NSSet 
- Construct
: NSArray가 빠르다. 중복체크를 할필요가 없기 때문
- iterating
: NSArray가 빠르다. 구성 방법이 NSSet보다 더 간단할 것이기 때문이다. 예를들어, NSArray는 단순한 Linked List로 구성되어 있다면 NSSet은 객체의 hash값을 테이블 형태로 저장할 것으로 보인다. (NSDictionary와 비슷한 형태로.. 왜냐하면 containsObject:를 써서 NSSet안에 있는 객체를 찾을때 객체의 hash값으로 O(1) 복잡도로 바로 찾도록 구현되어 있을것이기 때문)
- search
: NSSet이 빠르다. NSSet은 NSDictionary와 비슷하게 객체의 hash 값으로 찾기 때문에 0(1) 복잡도면 되지만, NSArray는 전체를 다 검색해야 되기 때문이다. 


NSInvocation
: Objective-C 메세지를 객체로 추상화 하는 클래스이다. 메시지 이름(selector), 시그니처(methodSignature), 메시지 수진자(target), 모든 인자 값(arguments)들을 포함하고, NSInvocation을 실행한 이후 리턴 값(return value) 까지도 담을 수 있다.
: 일반적으로 타깃과 액션을 지정해서 메시지를 보내는것보다 매우 번거로운 작업이다. 그러나  NSUndoManager 클래스와 비슷한 동작을 구현하는 것을 매우 쉽게 만들어 준다.

- 주의사항
1) 반드시 invocationWithMethodSignature 지정 생성자를 이용해서 생성하고 초기화를 해야 한다. (alloc, init 으로 하면 안됨)
2) -setArgument:atIndex: 메서드로 인자 값을 설정할 때 두 번째부터 인자 값을 지정해야 한다. 0번째와 1번째는 self, _cmd 이다. 
3) 넘기는 인자 값에 객체 참조가 있을 경우 효율성을 위해서 retain 하지 않는다. 따라서 인자 값으로 넘어가는 객체에 대한 소유권을 invoke시점까지 유지하고 싶으면 -retainArguments 메시지를 보내야 한다. 


메시지 포워딩
: 특정 객체에 구현되지 않은 메시지를 보내면 셀렉터와 일치하는 메서드가 없어서 오류가 난다. 이렇게 타깃 객체에 셀렉터와 일치하는 메서드가 없어도 에러를 발생시키지 않고 해결할 수 있는 방법은 다음과 같다

1) 동적 메서드 추가하기
: 우선 객체는 일치하는 메서드를 찾지 못하면 +resolveInstanceMethod:  메서드를 호출한다. 이 방법은 해당 메서드에 대한 동작을 하는 동적 메서드를 미리 구현해놓고, class_addmethod() 같은 런타임 API로 메서드를 추가하고 YES를 리턴하면 해당 메서드를 다시 호출한다.
 
2) 곧바로 포워딩 하기
: 다른 객체에게로 메시지를 포워딩 하는 것이다. 
-forwardingTargetForSelector: 메시지를 보내서 대신 수신할 객체가 있으면 리턴해주고 없으면 nil을 반환한다. 
이 메서드를 상위 클래스에 구현해놓고, 해당 메시지를 처리할 수 있는 내부 객체를 반환하는 형태로 구현하기도 한다. 

3) NSInvocation 사용해서 invoke
: 이 방법은 앞에도 설명했기 때문에 생략한다.


*  코어 파운데이션 객체들은 오픈소스로 애플 개발자 사이드 http://www.opensource.apple.com/source/CF에 공개되어 있다.







'IOS > 공통' 카테고리의 다른 글

Event Handling Guide - Responders  (0) 2017.05.03
Event Handling Guide - Gesture  (0) 2017.04.07
Cocoa Pods  (0) 2017.02.19
About Bundle  (0) 2017.02.14
iOS Architecture  (0) 2016.12.18
Posted by 홍성곤
,

Cocoa Pods

IOS/공통 2017. 2. 19. 17:46

1. ruby가 설치되어 있어야 한다. cocoapods는 루비에서 돌아가기 때문.
(OS X 10.7 이후에는 ruby가 포함되어 나오기 때문에 따로 설치 불필요.)
- sudo gem install cocoapods

2. setup
- pod setup --verbose (--verbose는 setup 과정을 로그로 찍어줌)

3. 해당 프로젝트 폴더로 이동 후 init.
- pod init (프로젝트 내에 Podfile이 생성됨.)
- open -a Xcode Podfile (textEdit로 열면 자동으로 인코딩 작업이 들어가 오류가 날 수 있으므로 xcode로 Podfile을 연다.)

4. 주석을 풀고 프로젝트에 맞게 세팅한다.
- platform :ios, "8.0"
- use_frameworks! (swift는 static Library가 지원이 안되기 때문에, 이 옵션을 꼭 넣어서 Pod이 해당 라이브러리를 동적 라이브러리로 생성하도록 해야 한다.)

platform :ios, "8.0" use_frameworks!

5.  Podfile에 사용할 library 이름, 버전 추가
- pod 'Alamofire', '2.0.2' 
- pod version은 Semantic versioning을 따른다
 : major.minor.bugfix(patch) 형식이다.
 : major 버전이 바뀌면 이전 버전이랑 호환이 안된다는 뜻이다. 즉, major 버전업은 함부로 하면 안된다. 
 : minor 버전은 기능이 추가되었지만 하위버전이랑 호환이 된다는 뜻이다. 별다른 작업없이 업데이트 해도 특별한 문제가 없다.
 : bugfix(patch) 버전은 말그대로 버그 픽스다. 
 : 단, 예외가 있다. 예를 들어 major버전이 0인경우. 이는 beta 프로젝트라는 것이다. 이 경우 minor 버전이 major 버전이 된다. 즉, 0.8.0 버전을 사용하고 있는경우, 0.9.0버전을 마음대로 업데이트 하면 안된다.

6. Xcode프로젝트 닫고 install
- pod install
- 해당 프로젝트와 pod 프로젝트가 포함된 workspace파일과 pod library들이 저장될 Pod폴더가 생겨난다.

* 추후 추가할 library나 버전 업데이트 할경우 podfile을 변경해서 다시 pod install 하면 된다. 


'IOS > 공통' 카테고리의 다른 글

Event Handling Guide - Gesture  (0) 2017.04.07
Objective-C, Swift 기초  (0) 2017.03.26
About Bundle  (0) 2017.02.14
iOS Architecture  (0) 2016.12.18
UIApplicationMain?  (0) 2016.02.18
Posted by 홍성곤
,

About Bundle

IOS/공통 2017. 2. 14. 18:48

패키지
: any directory라고 표현되고, finder는 패키지를 하나의 파일인것 처럼 보여준다.


번들
: 표준화된 구조를 가지고 있는 디렉토리라고 한다. 실행가능한 코드와 코드에 의해 실행되는 resources들을 담고 있다.


번들의 종류
- Application : 어플리케이션 번들은 코드와 앱을 launch할때 필요한 resource들을 다룬다.
- Frameworks : 프레임워크 번들은 dynamic shared library와 이것과 연관된 resource들을 다룬다. 예를들어 header file같은것들..
 - plug-in들(OS X)


번들의 생성
: 보통 번들은 우리가 직접 생성하지 않는다. 보통 Xcode에서 새로운 프로젝트를 만들면 그때 Xcode가 target에 맞는 번들을 생성해 준다. 물론 모든 target이 번들을 기본적으로 가지는것은 아니다. application, framework, loadable bundler target등이 자신과 매칭되는 번들을 가지고 있고, shell tool, static library target등은 번들을 기본적으로 가지지 않는다.


번들 관리
: Objective-C에서는 NSBundle 클래스를 통해 번들을 관리한다. C-based application들은 CFBundleRef를 통해 번들을 관리한다.

- 대부분의 Core Foundation, Cocoa type들과 단리 CFBundleRef와 NSBundle과 toll-free bridged 되지 않는다. 그러나 각자의 객체에서 bundle path를 추출할 수 있으며, 그것을 가지고 각자 객체를 만들 수 있다.







'IOS > 공통' 카테고리의 다른 글

Objective-C, Swift 기초  (0) 2017.03.26
Cocoa Pods  (0) 2017.02.19
iOS Architecture  (0) 2016.12.18
UIApplicationMain?  (0) 2016.02.18
디버깅 (Crash 핸들링)  (1) 2015.11.05
Posted by 홍성곤
,

iOS Architecture

IOS/공통 2016. 12. 18. 09:22

iOS 아키텍처는 4개의 layer로 이루어져 있다. 각 layer들은 frameworks를 포함하고 있다.

1. Cocoa Touch
- 대부분 UI관련 Framework들이 포함되어 있다. 대표적인 FrameWork가 UIKit Framework이다.

2. Media
- Graphics, Audio, Video 관련 Framework들이 포함되어 있다.

3. Core Services
- Core Foundation, Foundation Framework가 포함되어 있다. 이외에, iCloud, social media, networking frameworks등이 포함된다.

4. Core OS Layer
- iOS에서 사용할 수 있는 framework들 중 가장 low level feature다. APP에서 코드로 직접 이 layer를 사용하지 않더라도 다른 상위 layer에서 대부분 사용하는 frameworks들이다. security, external hardware accessory, bluetooth, local Authentication등에 관련된 framework들이 포함된다.

 

* 해당 layer에 어떤 framework들이 포함되어 있는지 보려면 아래 링크를 참조해라.
- https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/iPhoneOSTechOverview/Introduction/Introduction.html

'IOS > 공통' 카테고리의 다른 글

Cocoa Pods  (0) 2017.02.19
About Bundle  (0) 2017.02.14
UIApplicationMain?  (0) 2016.02.18
디버깅 (Crash 핸들링)  (1) 2015.11.05
[iOS 9] Search  (0) 2015.11.05
Posted by 홍성곤
,

UIApplicationMain?

IOS/공통 2016. 2. 18. 08:57

app이 시작하는 main함수이다.

과연 UIApplicationMain함수의 네개 파라미터는 무엇을 의미할까?

1. argc : argv 배열의 총 수. iPhone 앱에서는 1.

2. argv : C문자열 배열. iPhone 앱에서는 argv[0]에 애플리케이션 파일의 위치가 unix 경로로 들어갑니다.

3. principalClassName : 앱 운용에 사용하고 싶은 오브젝트의 클래스 명. UIApplication을 상속한 클래스여야만 함. nil을 지정한 경우 UIApplication이 사용됨.

4. delegateClassName: 앱 보조 운용에 사용하고 싶은 오브젝트의 클래스 명. AppDelegate를 상속한 클래스가 아니면 안됨. 어짜피 우리는 AppDelegate.m을 조작할 수 있기 때문에 상속해서 쓸 필요는 없는듯 보임.



'IOS > 공통' 카테고리의 다른 글

About Bundle  (0) 2017.02.14
iOS Architecture  (0) 2016.12.18
디버깅 (Crash 핸들링)  (1) 2015.11.05
[iOS 9] Search  (0) 2015.11.05
[IOS 9] 개요  (0) 2015.11.05
Posted by 홍성곤
,


개발 환경: XCode (6.4), iOS (8.4)

Crash 종류!

먼저, Crash 타입에는 크게 2가지 유형이 있다.

1. SIGABRT ( EXC_CRASH로 불리기도 함)

- Signal Abort, 해당 Signal은 프로그램 스스로 error를 감지하여 abort signal을 보내는 것이다.(This signal indicates an error detected by the program itself and reported by calling abort. )

- C stdlib중에 abort()라는 함수가 있다. 이 함수는 프로그램을 비정상적으로 종료시키는 함수로, 호출한 프로세스에게 SIGABRT를 날려서 프로그램을 종료시킨다. 즉 App이 프로그램 실행 도중 의도치 않은 동작을 감지 했을때 abort()를 호출하여 App을 종료시키는 것이다. 

- 이 경우는 Crash 원인을 찾기에 비교적 쉬운 타입에 속한다. 왜냐하면 App(프로그램)이 Crash 원인을 알고 있어서 Debug Output에 Crash 원인을 출력해주기 때문이다.


2. EXC_BAD_ACCESS( SIGBUS, SIGSEGV )

- SIGSEGV : signal segmentation violation, 할당된 메모리의 범위를 벗어나는곳에서 읽거나 쓰기를 시도할 때 발생됨.(프로그램 자신의 영역이 아닌 다른 영역의 메모리를 접근했을 때) 또는 readonly 메모리에 대하여 write를 시도했을때 발생되는 error signal 이다.

- SIGBUS : signal bus, SIGSEGV와 같이 메모리에 비정상적인 접근을 할 때 발생하는데, SIGSEGV는 valid한 메모리에 비 정상적인 접근을 할 때 발생하며, SIGBUS는 invalid한 메모리에 접근할 때 발생한다.

- 이 경우는 Crash 원인을 찾기가 어렵다. App(프로그램)이 Crash원인을 감지하고 App을 중단 시킨것이 아니기 떄문에 Debug Output에 Crash원인을 출력하지 않는다.

다행인점은, XCode가 업그레이드 되면서 EXC_BAD_ACCESS Crash가 일어나야 되는 상황도 미리 감지하여 SIGABRT Crash를 발생시키는 것으로 보인다. 실제로 old 버젼 XCode에서 EXC_BAD_ACCESS Crash가 일어나던 것이 최신 버젼에서는 SIGABRT Crash를 발생시키는 사례를 종종 볼 수 있다.

Crash 대처법

- 위에서 소개한 SIGABRT, EXC_BADACCESS Crash 둘다 XCode가 업데이트 되면서 왠만한 Crash들은 해당 code line 위치에서 break point가 걸린다. 하지만 그렇지 않은경우 해당 crash를 해결하기 위한 기본적인 팁을 소개하겠다.

1. SIGABRT

- 위 Crash는 SIGABRT 타입이기 때문에  debug output에 Exeption Reason이 출력된다. 하지만 해당 Exeption이 일어난 위치를 확인하고 싶을때는 All Exception BreakPoint를 설정해 준다.


- 왼쪽 Navigator에서 BreakPoint Navigator를 활성화 시킨다. 


- 왼쪽 하단에 '+' 버튼을 눌러 모든 Exception에 Breakpoint를 지정한다.


이제 SIGABRT Crash가 발생되기 직전 exception이 발생된 코드 라인에 break point가 걸려 있을것이다. 해당 기능을 사용하면 좀 더 빠른시간안에 crash원인을 찾을 수 있다.

2. EXC_BAD_ACCESS

- EXC_BAD_ACCESS Crash의 대부분의 원인은 release retain 메커니즘 오류이다. 보통 MRC 모드에서 많이 발생되나, ARC 모드에서도 간혹 발생된다.

- 위 스샷 처럼 Crash가 난 위치에서 break point가 잡히지 않았을때는 Zombie Objects를 이용한다.


- 메뉴 Product -> Scheme -> Edit Scheme 에서 Enable Zombie Objects를 활성화 한다.

- Zombie Objects가 하는 일은 메모리에 할당된 객체가 release될때 dealloc 시키지 않고 모니터링 하는 것이다. 해당 객체를 모니터링 하는 도중 프로그램이 객체에 접근하면 에러 메시지를 날려 어떤 객체에 invalid한 메모리 접근이 일어났는지 알려주는 것이다.


* 요약: main 함수에서 SIGABRT Crash가 발생되면 All Exception Break Point를 지정하고 다시 실행해서 정확한 Crash 위치를 확인해라, EXC_BAD_ACCESS Crash가 발생되면 Zombie Objects를 활성화 시켜 어느 객체에서 메모리 접근 오류가 발생했는지 확인해라.

* http://stackoverflow.com/questions/3327828/xcode-lldb-how-to-get-information-about-an-exception-that-was-just-thrown (exception print)



'IOS > 공통' 카테고리의 다른 글

About Bundle  (0) 2017.02.14
iOS Architecture  (0) 2016.12.18
UIApplicationMain?  (0) 2016.02.18
[iOS 9] Search  (0) 2015.11.05
[IOS 9] 개요  (0) 2015.11.05
Posted by 홍성곤
,