요새, XCode용 Plugin 을 하나 개발하고 있는데요, 

XCode는 Plugin FrameWork은 있지만, API를 공개하지 않습니다. 

Objective-C의 특성을 통해서, 공개하지 않는 Header File을 긁어 낼수가 있는데요, 
그런 신통 방통한 일을 해주는 녀석이 바로 class-dump 입니다. 

위 페이지에 들어 가시면 다운 로드 받으실수 있고요, /usr/loca/bin 이나 PATH 설정 되어 있는곳에 설치 하시면 됩니다.


class-dump는 cli command이기 때문에 Terminal 에서 사용하실수 있습니다. 

그럼 한번 마법을 부려서 XCode의 Header를 스윽 긁어 볼까요


뭐 이런식으로 나오게 됩니다. Class들이 뭐 있는지만 살펴 보려면, 


요런 녀석들이 들어 있군요 ㅎ 할 수 있는 일에 내용이 많이 없죠? 
그 이유는 바로 XCode가 Plugin 위주로 개발이 되어 있기 때문입니다. SCM , 심지어 Compiler부분까지 plugin화 되어 있죠. 

왠지 Hacker가 된것 같은 기분이 들지만, 더 낳은 개발을 위해 오늘도 달려 봅시다 ㅎㅎ


특정날짜 시간의 Date Object를 구할일이 있어서 찾아 봤습니다.

java.util.Calendar를 이용해서 Date값을 구할 수 있습니다. 우선 예제 소스를 보시면

Calendar cal = Calendar.getInstance();

cal.set(Calendar.HOUR,hour);

cal.set(Calendar.MINUTE,min);

cal.set(Calendar.SECOND,0);

cal.set(Calendar.MILLISECOND, 0);

Date expectedDate = cal.getTime();


이런식입니다.

Calendar Class의 Object는 SingleTone으로만 접근이 가능합니다. 그래서 
"new Calendar()" 이런식으로 사용할 수 없고 "getInstance()" Method를 통해서 SingleTone Object를 가져 옵니다. 

그리고 Key-Value Mechanism을 통해서 년/월/일/시/분/초/마이크로초 이런식으로 세팅이 가능합니다. 

SingleTone으로 관리 되기 때문에, 사실 위와 같은 코딩은 조금 위험합니다. 
년/월/일을 다른데서 세팅 했을수 있기 때문이죠.
그래서 사용하기 전에, reset을 해주는게 좋은데요.

cal.setTime(new Date());


저 같은 경우는  위와 같이 현재 시간으로 reset해서 사용합니다. 



자 오늘 제가 하고 싶은 일은,  UITextField에 숫자를 입력할때, ","를 자동으로 집어 넣는것입니다.
1111->1,111 이런식으로 표기가 되는것이죠.

일단 값 변경이 일어 나는것을 Catch 하는 방법은 두가지가 있습니다.

UITextFieddDelegate의  "textField:shouldChangeCharactersInRage:replacementString:" 을 이용하는 방법이 하나이고, 

두번째로, UITextFieldTextDidChangeNotification을 이용하는 것입니다.

UIKIT_EXTERN NSString *const UITextFieldTextDidChangeNotification;


우선, Notification을 사용하는 방식은 선호 하지 않는 방법이기도 하고,  Notification을 등록하면, [textField setText:] Method가 불러 질때 에도 불러짐으로 사용하지 않고,  UITextFieldDelegate Method를 구현해서 사용하도록 하겠습니다.

TextField 의 값 Setting을 UITextField 내부에서 하지 않고, 우리 쪽 소스에서 하도록 해야 합니다.
결국 shouldChange... 샬라 샬라에서 NO 를 줘서, 변경이 일어 나지 않게 합니다.

아래 소스는 UITextField 내부 구현이 아닌, 우리의 구현으로 기본적이 동작이 가능하게 하는 코드 입니다. 

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {

NSString *newText = [textField.text stringByReplacingCharactersInRange:range withString:string];

[textField setText:newText];

return NO;

}


여기에 newText를 우리가 원하는 포맷으로 변경해주는 Formatter를 삽입하여,  콤마를 자동입력하게 할수 있습니다.

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {

NSString *newText = [textField.text stringByReplacingCharactersInRange:range withString:string];

[textField setText:[DataFormatter moneyFormat:newText]];

return NO;

}


iOS에서 MoneyFormatting에 관한 내용은 http://krazie99.tistory.com/49 에서 참조 하시면 됩니다 :)
이를 응용해서, iOS에서 이런저런 Auto Format Completion을 비롯한 이런저런 자동완성기능을 할 수 있을꺼라 생각됩니다.

Sample소스 첨부하니, 보시고 궁금한건 언제든 문의 하세요 ~ 




Android File I/O를 살펴본 이유는 POJO 를 저장하기 위해서다. 

사실 SharedPreference를 통해서 POJO를 저장할 수 있길 기대 했지만, SharedPreference는 Java Object를 저장할 수 없게 되어 있다.

그래서 결국 POJO를 Serialize 해서, File로 떨구기로 결정했기에, Android File I/O에 대해서 살펴 보기로 하였다. 

Android에서 File I/O의 Target Directory는 두가지일수 있다. 
하나는 Internal Storage이고, 다른 하나는 External Stroage(SD Card)이다. 
POJO를 저장하는 건 보통 매우 작은 단위의 Data이기 때문에, Internal Storage로 결정했다.

File I/O 역시, Context Class를 통해서 해결한다. 
Context Class를 살펴 보면 openFileInput/openFileOutput이 있다. 

abstract FileInputStream openFileInput(String name)
Open a private file associated with this Context's application package for reading.
abstract FileOutputStream openFileOutput(String name, int mode)
Open a private file associated with this Context's application package for writing.

보통 Java Application 에서 FileInputStream을 직접 사용하는것과 다리 
Context의 openFileInput Method를 통해서 internal Storage에서 해당 App의 SandBox Directory로 부터 File Stream을 열수 있다. 

그외에 FileXXXXStream을 사용하는 방법은 일반 Java Programming과 동일 함으로 생략.

여기서 한가지 더 필요 했던게, 만든 파일을 어떻게 지울것인가 이다. 

지우는 Method도 Context Class에 있다. 

abstract boolean deleteFile(String name)
Delete the given private file associated with this Context's application package.
뭐 역시나 별로 어려운건 없다 ㅎ

Android는 Context단위로, 만위 움직이기때문에, API 사용시에 Context를 사용할 일이 많다. 
그래서 JUnit에서 Context의 접근은 꼭 필요 한일이다. 

결론부터 말하면 org.junit.TestCase대신에 android.test.AndroidTestCase 를 사용하라는 것이다. 


위에서 보는 바와 같이 AndroidTestCase에서는 이미 Mock Context Object를 제공하고 있음으로,
AndroidTestCase 를 상속받아서, TestCase Class를 정의하고, 
내부적으로 getContext() Method를 이용해서 바로 쓰면 된다. 


Android에서, 화면은 Activity단위로 움직인다.

특정 Context에서 또다른 Activity를 실행시키고, 현재의 Activity를 종료 시키는 방법에 대해서 알아 보려고 한다. 

Context Class를 찾아 보면 startActivity라는 녀석이 있다. Context Class에 정의 되어 있기 때문에, Context의 SubClass인, Activity는 물론 Service Object에서도 특정 Activity를 실행 시킬 수 있다. 

public abstract void startActivity (Intent intent)

Since: API Level 1

Launch a new activity. You will not receive any information about when the activity exits.

Note that if this method is being called from outside of an Activity Context, then the Intent must include the FLAG_ACTIVITY_NEW_TASK launch flag. This is because, without being started from an existing Activity, there is no existing task in which to place the new activity and thus it needs to be placed in its own separate task.

This method throws ActivityNotFoundException if there was no Activity found to run the given Intent.

Parameters
intentThe description of the activity to start.
Throws
ActivityNotFoundException

startActivity()를 통해서, activity를 실행시키게 되면, 현재의 Activity 는 Pause상태에 접어 들게 된다. 

특정 Activity()를 종료 시키기 위해서는 Activity Class내의 finishXXX() Method들을 사용하면 된다. 
가장 기초적인것만 살펴보면, finish()가 있다. 

public void finish ()

Since: API Level 1

Call this when your activity is done and should be closed. The ActivityResult is propagated back to whoever launched you via onActivityResult().


finish() 가 호출되면, 현재 Activity가 Destroy 상태가 되며, (OnDestroy() 호출됨 )  이전 Activity 가 다시 Active상태가 된다 ( onResume() 호출됨 )

이런식으로 대강 Activity의 흐름이 흘러간다. 

나중에 finishActivity(Intent, int ) 와 같은 함수들이 쓸일이 생기면 그때 걔네들을 좀 살펴봐야 겠다.



일단 Strings 에 정의된 String값들도, layout등과 같이 각자의 식별자를 가진다. 

<?xml version="1.0" encoding="utf-8"?>

<resources>

    <string name="hello">Hello World</string>

</resources>


위와 같이 정의된 string파일이 있다면, "Hello World"라는 String은 "R.string.hello" 에 id가 정의된다. 

Java Source Code에서 string자원을 접근할때는,  Context Class의 getString() Method를 사용한다. 
 

위에서 보는 바와 같이 각종 Activity/Service등이 모두 Context의 SubClass임으로, 
Activity나 Service를 구현할때는 바로 getString을 사용할 수 있다.

아래 코드는 "R.string.hello"을 사용하는 예제 코드 이다 .

someTextView.setText(getString(R.string.hello));




<EditText android:layout_height="wrap_content" 

android:id="@+id/TimeEditText" android:layout_weight="1" android:layout_width="fill_parent"

android:inputType="number"

>

</EditText>


android:inputType으로 입력받을 수 있는 값을 강제 할수 있다. 생각보다 쉽죠? ㅎ
그러나,  맘에 안드는게 있는데, AVD에서 보면

우외 같이 키보드가 다 나온다는 얘기다. 
터치를 누를수는 있는데, 값은 입력이 안된다. 

그럼 이렇게 숫자만 있는 것을 어떻게 사용할까? 이건 뭐 Java 기초 ㅎ

Integer.parseInt(String .. ) 를 이용한다. 

EditText editText = (EditText) mAlertLayout.findViewById(R.id.TimeEditText);

mins = Integer.parseInt("" + editText.getText());


editText의 값이 null 일것을 대비해서 "" + 를 붙혀서 넣는걸 잊지 말자. 


Twitter API를 구현하는건 매우 귀찮은 일이라 ㅎ
일전에 사용해본 경험이 있는 MGTwitterEngine을 사용해보고자 한다. 

내가 처음 MGTwitterEngine을 사용해본게, 아마도 1년전 즈음이었으니, 소스 구조도 빌드 방법도 많이 바뀌었구나,
내 소감부터 말하면 완전 그지 같다. 

외부 라이브러리를 가져다 쓰니까 외부 라이브러리를 빼놓은것은 이해하지만 그럼 BuildScript를 통해서
Dependency를 좀 해결할수 있게 해주던가 :(

Reference가 깨져서 빨간불 잔뜩이다. 

https://github.com/mattgemmell/MGTwitterEngine

위의 링크를 통해 MGTWitter Engine을 컴파일 해보면, 아래와 같이 에러가 잔뜩이다. 

일단 좌측에 소스가 없는것에 TouchJSON/OAuthConsumer/yaji 가 있는데, OAuthConsumer를 제외하고는 Optional Parser이기 때문에, 아예 Reference를 지워 버리자. 

OAuthConsumer는 OAuth에 필요 하기 때문에 따로 다운로드 받아서 Reference Error를 해결해주도록 하자.
다운로드 링크는 아래와 같다. 

https://github.com/ctshryock/oauthconsumer

다운로드 가 끝나고 나면 OAuthConsumer Group의 Info를 열어서 "Choose" 버튼을 눌러서,
다운로드 경로로 바꿔주자.

그런데, 여전히 빨간불 들어 오는 녀석이 있는데, 이건 MGTwitterEngine쓴애가 오타를 써서 그렇다.
Crypto 가 맞는데 Crytpo로 되어 있다. 
Crytpo Group의 Info를 바꿔서 다운받은 OAuthConsumer 소스 내의 Crypto 로 Path를 변경해주자.

그리고 여전히 한군데 빨간불이 있는데, 


그냥 일단 지워버리자 ㅋㅋ

자 그런 소스구조내의 빨간불은 다 해결했으니, 빌드 ! 그러나 현실은 에러 ㅋㅋㅋ

위의 에러는 우리가 사용하지 않을 yajl 과 TouchJSON 쪽에너 내는데, 
MATwitterEngine내의 "Twitter TouchJSON Parser" 와 "Twitter YAJLParsers" Group을 삭제 하자.


그러고 빌드 하면, 빌드 성공 !


우여 곡절끝에 빌드를 완료 했다.

빌드가 완료되었으니, 소스를 긁어다가 사용하든, 라이브러리화 해서 사용하든 하면 된다.

나는 내가 만든 소스가 아닌 경우 보통 라이브러리화 시켜서 사용하는 편인데, 
일단 귀찮고, 당장에 쓸일도 없어서,  소스 긁어다가 샘플 소스 만든 것 첨부하도록 하겠습니다. 


MGTwitterEngine개발하신분은 정말 고맙지만, 좀 이해하기 힘든 방식으로 배포 하는것 같습니다. 
이렇게 쓰기 귀찮아서야, 버전 업그레이드 된다고 해도 Major 이슈가 없다면, 업그레이드 하지 않을것 같네요.

사실 Major이슈라면, 그냥 제가 고쳐서 -_-+
다들 즐거이 개발하세요 :)

OCUnit 에서 UIImage 의 imageNamed:를 호출하면 
메인 어플과 다르게 UIImage Object를 가져 오지 못하고, nil 을 리턴하면서 프로그램이 좌자작 꼬인다. 

왜 이런 문제가 나타나는 걸까 ?

그건 바로 mainBundle이 꼬여 있기 때문이다. 
OCUnit FrameWork 단에서 mainBundle을 OCUnit Product로 변경해줬어야 할것 같은데, 그게 되어 있지 않다. 

그래서 OCUnit에서 Bundle Resource를 사용할때는 [NSBundle mainBundle]을 사용하지 않고, bundleForClass Method를 사용해서 접근하는데, UIImage imageNamed:는 내부적으로 mainBundle 을 호출하기때문에 이를 피해갈 방법이 없다. 

그렇다면 UIImage imageName:를 쓰지 말아야 하는것일까? 
그렇게 해야만 했다면, Blog에 Posting하지도 않았겠죠? ㅎㅎ

아이디어는 Objective-C의 Category 라는 특성을 이용해서 Method만 Override하는것이다.
( 사실 Method Implementation Switch 로 할려고 했는데,, 잘 안되더라고요 :( ) 

그럼 UIImage 의 imageName:을 Override할것인가 NSBundle의 mainBundle을 Override할것인가를 결정해야 하는데,

TestCode내에서 mainBundle말고, bundleForClass를 부르는것도 짜증났기에, 
NSBundle의 mainBundle 을 재정의 하기로 했다.

소스코드는 매우 간단하다.

@implementation NSBundle(OCUnit)


+(NSBundle *) mainBundle {

return [NSBundle bundleForClass:[TestClass class]];

}

@end


여기서 좀 마음에 들지 않는 부분이 있는데, 
Test하고 있는 Class를 참조 해야 한다는것이다.
<주의 ! TestClass의 Class명은 OCUnit내의 Class로 이름을 변경해주어야함 >
 
결국은 bundleForClass를 통해서 하고 있는데, 좀 다르게 깔끔하게 하는 방법을 좀 찾았으면 좋겠다.
아마도 있겠지만, 일단은 흘러가니, 나중에 라이브러리화를 고려할때, 좀더 생각해봐야 겠다.





+ Recent posts