'xcode'에 해당하는 글 28건

원문: https://medium.com/developerinsider/best-ios-development-tips-and-tricks-6c42c1d208c1

이 글은 먼저 DeveloperInsider에 발행됩니다. 여기(원문)서 확인할 수 있습니다.

1. Xcode에서 빌드시간 확인하기

프로젝트의 정확한 빌드 시간을 모른다면 Xcode에서 아래 옵션을 켜자.

defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES

2. Swift 프로젝트 빌드시간 단축시키기

Xcode 9.2 릴리즈 노트에서는 BuildSystemScheduleInherentlyParallelCommandsExclusively 사용자 디폴트를 켜서 스위프트 빌드시간을 단축시켜주는 실험단계의 기능을 언급했습니다.

defaults write com.apple.dt.Xcode BuildSystemScheduleInherentlyParallelCommandsExclusively -bool NO

주의: 릴리즈 노트에 따르면 "빌드시간동안 메모리 사용을 증가시킬 수 있는 실험적 기능"이라고 했습니다.

3. Xcode에서 전체화면 모드로 시뮬레이터 사용하기

나는 Xcode 9 기능중에 전체화면으로 iOS 시뮬레이터와 Xcode를 실행시키는 것을 좋아한다. 이 기능을 사용하려면 그냥 터미널을 켜서 아래 명령을 실행시키면 된다.

defaults write com.apple.iphonesimulator AllowFullscreenMode -bool YES

시뮬레이터에 숨겨진 더 많은 기능을 사용하고싶으면 애플의 숨겨진 Internals 메뉴를 활성시키면 된다. 이렇게 하기 위해서는 루트 폴더에 AppleInternal이라는 빈 폴더를 만들자. 아래 명령을 실행시키고 시뮬레이터를 재시작하면 된다.

sudo mkdir /AppleInternal

4. iOS 시뮬레이터 화면 녹화하기

xcrun 명령어 유틸리티를 이용하면 시뮬레이터 창을 스크린샷 찍거나 비디오로 저장할 수 있습니다. 비디오를 찍기 위해서는 아래 명령을 실행시키세요.

xcrun simctl io booted recordVideo <filename>.<file extension>

예시:

xcrun simctl io booted recordVideo appvideo.mov

녹화를 멈추려면 control + c를 누르십시오. 파일이 만들어지면 현재 폴더에 저장됩니다.

5. 파인더에서 시뮬레이터에 파일 공유하기

Xcode 9부터는 시뮬레이터가 파인더 확장을 가지고 있습니다. 이것은 파이더 창에서 바로 파일을 공유할 수 있게 해줍니다. 그래도 파일을 시뮬레이터로 드레그&드롭해 주는게 더 빠른거 같네요.

아니면 아래 simctl 명령으로 이미지/비디오 파일을 전송할 수 있습니다.

xcrun simctl addmedia booted <PATH TO FILE>

6. sudo 인증시 지문 사용하기

맥북프로의 지문을 sudo 인증의 비밀번호로 사용하고 싶다면 /etc/pam.d/sudo를 수정하고 상단에 아래줄을 넣습니다.

auth sufficient pam_tid.so

이제 sudo로 지문을 사용할 수 있습니다.

7. 소리 알림과 함께 AutoLayout 컨스트레인트 디버깅하기

이번 방법은 AutoLayout 컨스트레인트를 디버깅하는데 훌륭한 벙법입니다. 그냥 실행시 UIConstraintBasedLayoutPlaySoundOnUnsatisfiable 인자를 넘겨주면 런타임시 컨스트레인트가 어긋날을 경우 소리가 납니다.

-_UIConstraintBasedLayoutPlaySoundOnUnsatisfiable YES

8. Xcode에서 사용할 수 없는 시뮬레이터 제거하기

이 조그만 명령어는 Xcode에서 사용할 수 없는 모든 시뮬레이터를 제거해 줍니다. 여기서 "unavailable"은 Xcode의 xcode-select 버전에서 사용불가능한 것을 의미합니다.

xcrun simctl delete unavailable

재밌었나요? 아래에 댓글을 달아주시면 고맙겠습니다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 - 블로그 댓글 - 페이스북 페이지(@나는한다번역-ios) - 트위터(@tucan9389) 으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

출처: [애플 공식 문서] Integrating a Core ML Model into Your App(원문)

앱에 간단한 모델을 추가하고, 모델에 입력 데이터를 넣은 뒤, 모델 예측을 실행시키는 예제입니다.

프로젝트 다운로드

SDKs

iOS 11.0+

Xcode 9.2+

Framework

Core ML

On This Page

개요

함께 보기

개요

이번 간단한 앱은 학습된 간단한 모델 (MarsHabitatPricer.mlmodel)을 사용하여 화성거주지 가격을 예측할 것이다.

여러분의 Xcode 프로젝트에 모델 넣기

모델을 Xcode 프로젝트에 넣기 위해서는 모델을 드레그해서 프로젝트 네비게이터에 넣어야한다.

Xcode에서 모델을 열면 모델 타입, 예상되는 입출력등 모델에대한 정보를 확인할 수 있다. 여기서 모델 입력값은 태양열 전지판과 온실 갯수, 그리고 거주지 크기(에이커 단위)이다.

코드에서 모델 생성하기

Xcode는 모델의 입출력 정보를 이용하여 자동으로 모델의 인터페이스를 생성하는데, 이 인터페이스는 여러분의 코드상에서 모델과 상호작용하는데 쓰일 것이다. MarsHabitatPricer.mlmodel의 경우 Xcode는 MarsHabitatPricer모델을 표현하는 인터페이스와 모델의 입력(MarsHabitatPricerInput), 모델의 출력(MarsHabitatPricerOutput)을 생성한다.

모델을 생성하려면 만들어진 MarsHabitatPricer 클래스 생성자를 사용하자.


let model = MarsHabitatPricer()

모델에 넣을 입력값 가져오기

이번의 간단한 앱에서는 사용자로부터 모델의 입력값을 가져오기위해 UIPickerView를 사용한다.


func selectedRow(for feature: Feature) -> Int {
   return pickerView.selectedRow(inComponent: feature.rawValue)
}

let solarPanels = pickerDataSource.value(for: selectedRow(for: .solarPanels), feature: .solarPanels)
let greenhouses = pickerDataSource.value(for: selectedRow(for: .greenhouses), feature: .greenhouses)
let size = pickerDataSource.value(for: selectedRow(for: .size), feature: .size)

예측을위해 모델 사용하기

MarsHabitatPricer 클래스는 자동으로 생성된 prediction(solarPanels:greenhouses:size:) 메소드를 가지는데, 이 메소드는 모델 입력값으로부터 가격을 예측하는데 쓰인다. 이번의경우 태양열 전지판과 온실 갯수, 그리고 거주지 크기(에이커 단위)를 넣는다. 이 메소드는 MarsHabitatPricerOutput 인스턴스를 결과로 내뱉는다.


guard let marsHabitatPricerOutput = try? model.prediction(solarPanels: solarPanels, greenhouses: greenhouses, size: size) else {
   fatalError("Unexpected runtime error.")
}

예측된 가격을 가져오기위해 marsHabitatPricerOutputprice프로퍼티에 접근하여 앱 UI에 표시하자.


let price = marsHabitatPricerOutput.price
priceLabel.text = priceFormatter.string(for: price)
주의

자동으로 생성된 prediction(solarPanels:greenhouses:size:) 메소드에서 에러를 던질수 있다. Core ML으로 작업하면서 만나게될 가장 일반적인 에러 종류는 입력 데이터의 내용과 모델이 기대하는 내용이 서로 일치하지 않는 경우입니다(가령 잘못된 이미지 포멧이라던지).

Core ML 앱을 빌드하고 실행하기

Xcode는 Core ML 모델을 디바이스에 최적화시켜 컴파일한 후 리소스 안에 넣어둘 것입니다. 최적화된 모델은 앱번들안에 들어가며, 이 모델은 앱이 디바이스에서 실행되는동안 예측을 만드는데 쓰입니다.

함께 보기

첫번째 단계



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: Catching Leaky View Controllers Without Instruments

레테인 사이클(retain cycles)에의해 생긴 메모리 누수를 찾아내는 방법 중 잘 알려진 방법은, 더이상 화면에 있지 않을때 모든 뷰컨트롤러들이 디얼록되었는지 확인하는 것이다. 이 과정은 각각이 할당해제되기 전에 직접 반복해서 시행해보아야하는데, 재밌지도 않을 뿐더러 여기서 에러를 만들어내기도 한다. 만약 좀 더 일찍 UIViewController 누수에대한 
과정을 알 수 있었다면, 매일 개발하는동안 훨씬 낫지 않겠는가?

UIViewController의 잘 알려지지 않은 두개 프로퍼티에대해 감사하게 될 수도 있다.
  • isBeingDismissed: 모달로 나타난 뷰컨트롤러가 dismiss될 때, 이 프로퍼티는 true이다.
  • isMovingFromParentViewController: 부모 뷰컨트롤러로부터 이 뷰컨트롤러가 제거될 때, true가 된다. 이것은 UINavigationController 스택에서 뷰컨트롤러가 pop될 때와 같은, 시스템 컨테이너에서 제거되는 것도 포함된다.
이 프로퍼티 중 하나가 true면, 곧 뷰컨트롤러가 디얼록될 수 있다는 것을 알 수 있다. 정확히 얼마나 있어야 내부적으로 정리된 상태를 만들고 ARC가 디얼록할지는 모른다. 간단하게 생각해서 2초보다 더 길지는 않을 것이라 가정하자.

우리가 생각한 것을 코드로 만들어보자.
extension UIViewController {
   public func dch_checkDeallocation(afterDelay delay: TimeInterval = 2.0) {
       let rootParentViewController = dch_rootParentViewController

       // We don’t check isBeingDismissed simply on this view controller because it’s common

       // to wrap a view controller in another view controller (e.g. in UINavigationController)

       // and present the wrapping view controller instead.

       if isMovingFromParentViewController || rootParentViewController.isBeingDismissed {
           let type = type(of: self)
           let disappearanceSource: String = isMovingFromParentViewController ? "removed from its parent" : "dismissed"

           DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { [weak self] in

               assert(self == nil, "\(type) not deallocated after being \(disappearanceSource)")
           })
       }
   }

   private var dch_rootParentViewController: UIViewController {
       var root = self

       while let parent = root.parent {
           root = parent
       }

       return root
   }
}
흥미로운 부분은 asyncAfter(deadline:execute:) 호출에서 일어난다. 먼저 sefl를 weak로 만들면([weak self]), 나중에 호출할 클로저에의해 리테인되지 않는다. 그리고 self가 nil이면 assert를 건다(여기서 self는 UIViewController). 뷰컨트롤러가 살아있게 잡아두는 리테인 사이클을 가질때만 nil이 아니다.

이제 마지막으로 해야할 일은 모든 뷰컨트롤러의 viewDidDisappear(_:)에서 dch_checkDeallocation()을 호출하는 것이다(부모로부터 제거되거나 dismiss된 후에도 살아있도록 잡아두는 것을 제외하고)
override func viewDidDisappear(_ animated: Bool) {
   super.viewDidDisappear(animated)

   dch_checkDeallocation()
}
만약 누수가 일어나고 있으면, assert에서 실패나 나올 것이다(-Onone 빌드에서만 가능).

여기서 우리는 그냥 메모리 그래프 디버거(Memory Graph Debugger)를 열어 사이클의 원인을 찾고 고치면 된다.

이 방법을 통해 새로 소개된 리테인 사이클을 빠르게 배우는데 도움이 될거라 생각한다. 여러분도 이 방법이 즐거웠길 바란다! production-ready 코드(더 많은 주석과 if DEBUG 확인으로)는 GitHub의 DeallocationChecker에서 확인해볼 수 있다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: iOS — Identifying Memory Leaks using the Xcode Memory Graph Debugger

이 짧은 포스팅에서 내가 설명할 것은 아래와 같다
  • Xcode의 메모리 그래프 디버거란
  • 이것을 어떻게 사용하고, 몇가지 팁들
  • 장/단점들

이것이 무엇인가
짧게말해 메모리 그래프 디버거는 다음 질문의 답변에 도움을 준다. 한 오브젝트가 왜 메모리에 남아있는가?

Xcode의 메모리 그래프 디버거는 레티인 사이클과 메모리 누수를 찾아내고 고치는데 도움을 준다. 그 일이 발생하면, 앱 실행이 일시정지되고 현재 힙에서 오브젝트를 보여주는데, 이 오브젝트가 살아있게 하는 참조들이 무엇인지 그 관계와 함께 나타난다.


어떻게 사용하는가
리테인 사이클과 메모리 누수를 식별하는데 3가지 간략한 단계가 있다.
  • 1. 아래처럼 Xcode scheme editor로 stack logging에 체크한다.

live allocations을 위해 Malloc stack logging을 켜기live allocations을 위해 Malloc stack logging을 켜기


'Live Allocation'의 logging만 켰다. 이것은 디버깅할때 'All Allocations'를 선택하는것보다 오버헤드가 적고, 레테인 사이클과 누수를 식별하는데 필요한 것이다.
  • 2. 분석하고 싶은 앱을 실행시키고(리테인 사이클이나 누수가 의심되는 행동), 그 디버그 바 버튼을 선택하여 메모리 그래프 디버깅 모드로 들어가자.

메모리 그래프 디버깅 버튼메모리 그래프 디버깅 버튼

  • 3 .메모리 그래프 디버거는 앱 실행을 일시정지하고 아래를 보여준다.

Xcode의 메모리 그래프 디버거 모드Xcode의 메모리 그래프 디버거 모드

왼편에 디버그 네비게이터가 앱의 힙 내용을 보여준다.

디버그 네비게이터에 type/instance를 선택하면 가운데 패널에서 인스턴스 참조들을 보여준다.

가운데 참조 패널에 인스턴스를 선택하면 오른편의 인스팩터 페널에서 일반적인 오브젝트 메모리 정보와 allocation backtrace를 보여준다.

누수는 아래처럼 디버그 네비게이터에서 볼 수 있다.

디버그 네비게이터에 나타난 누수디버그 네비게이터에 나타난 누수


  • 1. 메모리 누수를 식별하는데 도움이 되기위해서, 아래처럼 누수만 보이도록 힙 내용을 필터링할 수 있다.

메모리 누수를위한 필터링메모리 누수를위한 필터링

  • 2. 런타임 이슈 네비게이터도 유용한데, 식별된 모든 누수의 숫자를 표시한다.

수많은 메모리 누수들!수많은 메모리 누수들!


좋은점과 나쁜점
  • 좋은점: 운좋게 누수를 쉽게 찾아낼 수 있다(간단한 리테인 사이클). 예를들어 한 오브젝트가 클로저 프로퍼티 안에서 자신을 붙잡고 있을때. 이건 붙잡고 있는 참조를 weak로하여 쉽게 고쳐진다.
  • 나쁜점: 잘못 알려주는 경우. 예를들어 UIButton 오브젝트를 만들고 UIToolBars 아이템 배열에 추가했는데, 메모리 누수로 나왔지만 그 이유는 볼 수가 없었다.


유용한 링크들

이게다다! 📱🚀👍🏽


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: System Level Breakpoints in Swift


모든 훌륭한 소프트웨어 개발자들은 결국 훌륭한 소프트웨어 디버거가 되야한다. 디버깅은 크게는 브레이크포인트의 설정으로 이루어지고, 실행시간동안 앱의 임의의 상태 지점을 관찰하기위해 브레이크 포인트를 걸어 놓은 곳에 가본다. 큼직하게는 두가지 종류의 브레이크 포인트가 있다. 하나는 당신의 코드에 설정한 것이고, 다른 하나는 다른사람의 코드에 설정한 것이다.

여러분의 코드에 브레이크 포인트를 설정하는것은 간단하다. 그냥 Xcode 프로젝트의 소스코드 라인을 찾아서, 관련된 라인 옆에 홈 안의 공간을 탭하면 된다.



그러나 시스템 API에대해 브레이크 포인트를 설정하고 싶거나 소스코드를 가지고 있지 않은 라이브러리 안에 구현된 메소드에 브레이크 포인트를 설정하고 싶은 경우는 어떨까? 예를들어, 레이아웃 버그를 잡아야한다고 할때, UIView에서 애플의 고유의 내부 layoutSubviews 메소드 호출을 관찰하는데 도움이 될 수 있다. 역사적으로 Objective-C 개발자들에게는 이것이 큰 문제가 아니다. 어떤 메소드를 심볼릭하게 표현하기위해 그리고 그것을 브레이크하기위해, Xcode의 lldb 콘솔(View -> Debug Area -> Activate Console)로 들어가서 이 이름을 지정해가면서 브레이크 포인트를 지정하면 된다는 것을 안다. lldb에서 "b"라는 단축 명령어는 우리가 입력한 것으로부터 전체 이름과 매칭되는 것을 찾는 정규식이다.

(lldb) b -[UIView layoutSubviews]
Breakpoint 3: where = UIKit`-[UIView(Hierarchy) layoutSubviews], address = 0x000000010c02f642(lldb)

lldb 콘솔에서 겁을 주거나, 현재 디버그 세션보다 더 길게 붙이는 브레이크 포인트를 원하면, Xcode의 내장된 심볼릭 브레이크 포인트 인터페이스(Debug -> Breakpoints -> Create Symbolic Breakpoint)를 사용하여 같은 결과를 달성할 수 있다.


사실, 여러분의 iOS 앱에 브레이크 포인트를 걸고 앱을 돌리면 애플의 layoutSubviews 메소드에서 브레이크 포인트 안으로 실행시켜볼것이라고 생각한다 lldb 콘솔로 돌아와서 메시지를 받은 오브젝트를 확인한다.

(lldb) po $arg1
<UIClassicWindow: 0x7f8e7dd06660; frame = (0 0; 414 736); userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x60000004b7c0>; layer = <UIWindowLayer: 0x600000024260>>

이제 계속해서 심블에 브레이크를 걸어보자. 그리고 또다시 그렇게 하자. lldb 콘솔에 "po $arg1"이라고 입력하여 매번 타겟을 확인하라. 유별난 버그를 추적하는동안 이런 종류의 분석을 시행하는게 얼마나 유용한지 상상할 수 있을 것이다.

그러나 우리 플랫폼을 이제 시작한 가여운 스위프트 프로그래머들이나 스위프트 문법에만 열관하는 사람들은 어떻게할까? 애플의 문서를 읽어보거나, "-[UIView layoutSubviews]"를 해석하는게 불가능한 사람들은 "UIView.layoutSubviews"이 완전히 눈에 거슬릴 뿐만 아니라, 스위프트에게는 옳은 것일까?

불행히도 "UIView.layoutSubviews"라고 브레이크포인트를 설정하면 동작하지 않는다.

(lldb) b UIView.layoutSubviews
Breakpoint 3: no locations (pending).WARNING:  Unable to resolve breakpoint to any actual locations.
(lldb)

이것이 실패하는 이유는, UIView에 스위프트로 구현된 layoutSubviews라는 메소드가 없기 때문이다. 이것은 모두 Objective-C로 구현되있다. 사실 아주 많은 스위프트에서 쓸 수 있는 Objective-C 메소드들이 Objective-C 메시지 전송으로 바로 전달하도록 컴파일된다. 스위프트 파일에 "UIView().layoutIfNeeded()"같은 것을 입력하면, 컴파일은 될것이며, layoutIfNeeded라는 스위프트 메소드가 없기때문에 호출해도 아무일도 일어나지 않을 것이다.

이것이 스위프트로 맵핑된 모든 Cocoa 타입들에게 해당되지는 않는다. 예를들어, 모든 "Data.write(to:options:)" 호출에 브레이크를 걸고 싶다고 생각하자. 아마 "Data.write"에 브레이크 포인트를 걸어서 동작하길 원할것이다.

(lldb) b Data.write
Breakpoint 11: where = libswiftFoundation.dylib`Foundation.Data.write (to : Foundation.URL, options : __ObjC.NSData.WritingOptions) throws -> (), address = 0x00000001044edf10

그리고 된다! 이건 어떨까? 실제로 이것만 아니다. 이것은 -[NSData writeToURL:options:error:]의 길에서 libswiftFoundation을 통해 전달하는 모든 호출에 브레이크가 걸릴 것인데, Objective-C 구현을 호출하는것에 직접 잡히지는 않을 것이다. 메소드에서 모든 호출을 잡기 위해서는, 저수준(Objective-C 메소드)에서 브레이크포인트를 설정할 필요가 있다.

따라서 규칙으로, iOS나 Mac 플랫폼에서 더 좋은 디버거를 원하는 스위프트 프로그래머들은 Objective-C의것과 동일하게 스위프트 메소드를 매핑할 수 있는 능력이 필요하다. UIView.layoutSubviews같은 메소드의경우, 바로 "-[UIView layoutSubviews]"로 맵핑하지만, 많은 메소드의경우 지금처럼 간단하게 될것이다.

스위프트로 맵핑된 메소드 이름을 Objective-C로 다시 매핑하기위해서는, 많은 Foundation 클래스들이 NS 접두를 뺐다는 것을 인식하고, 스위프트 API 가이드라인을 적용하기위해 메소드 시그니처를 다시 작성한 영향이다. 예를들어 순수한 스위프트 프로그래머들은 "Data.write(to:options)"의 저수준 구현으로 브레이크 포인트를 설정하기 때문에 쉽게 유추하지 못할것이다. "NS" 접두를 붙이고, URL 파라미터를 명시적으로 넣고, 이상한 에러 파라미터를 추가한다. 이것은 보기에 옛날의 나쁜 시기에 까탈스러운 백발의 노인이 실패를 전달하는데 사용하는 것 같아 보인다.

(lldb) b -[NSData writeToURL:options:error:]
Breakpoint 13: where = Foundation`-[NSData(NSData) writeToURL:options:error:], address = 0x00000001018328c3

성공했다!

이런 Obejctive-C 메시지 시그니처와 API 규약들에대한 추가적인 지식을 개발하게 만드는 생각은 여러분을 힘겹게 만든다는 점에서, 나는 여러분의 다음 과제를 통해 얻을 약간의 수정된것을 제공한다(I offer a little hack that will likely get you through your next challenge). API가 이런 원리중 하나를 사용햐여 작성되었으면, 함수의 스위프트 이름은 거의 Objective-C 메소드 이름의 부분집합이다. 아마도 lldb의 정규식 일치 기능을 활용하여 브레이크포인트를 설정하고싶은 메소드를 0으로 만들 수 있다.

(lldb) break set -s Foundation -r Data.*write
Breakpoint 17: 8 locations.

이제 "break list"를 입력하고 lldb가 표시한 매칭 수를 보자. 그것들 중에는 스위프트로 된 (libswiftFoundation으로 이루어진) 여러 메소드가 있으나, 질문에서 타겟 메소드를 찾아야할 것이다. 사실, 여러분이 브레이크를 걸고싶은 다른 저수준의 Objective-C 메소드도 찾아봐야할 것이다.

리스트를 더 잘 관리하기위해, 주어진 Objective-C 프레임워크안에 타겟 메소드가 있다는 지식을 주고, 이름으로 특정 공유된 라이브러리에 제한된 매칭을위한 "-s" 플래그를 넣는다.

(lldb) break set -s Foundation -r Data.*write
Breakpoint 17: 8 locations.

이 브레이크포인트 사이에서 NSPageData에서 몇 거짓이 있지만, 그 목록은 모두 더 관리할 수 있다. 하나의 브레이크포인트 "17"은 하위-숫자로 식별된 매치의 모든것을 가지고 있는다. 여러분의 방법으로 브레이크포인트의 목록을 걸러내도 좋다.

(lldb) break disable 17.6 17.7 17.8
3 breakpoints disabled.
(lldb) c

Objective-C를 스위프트에 맵핑하는 애플의 방식은 스위프트 개발자를위해 더 즐거운 프로그래밍 경험을 함께 만들게 해주지만, 그 세부적인 구현을 이해하지 못하고 있거나 어떻게 동작하는지에대한 이해가 부족하게되면 엄청난 혼란으로 빠질 수 있다. 나는 이 글이 여러분의 스위프트 앱을 디버깅하는데 필요한 도구가 되었으면 좋겠고, 불가피하게 사용하는 Objective-C 코드를 더 효율적이게 썼으면 좋겠다.

Update: 나는 두가지 관련된 버그를 넣었다: Radar #31115822 스위프트 메소드 포맷에서 자동으로 매핑한 것을 Obejctive-C 메소드로 돌아오도록 해야한다, 그리고 Radar #31115942 간결한 스위프트 메소드 시그니처를 만드는것에대해 lldb를 더 직관적으로 만들도록 해야한다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,





애플은 이번 2016년 WWDC에서 Xcode Source Editor Extensions를 소개했다. 내가 좋아하는 툴을 쉽고 간편한 방법으로 확장할 수 있게 해준다. 이것이 왜 생산성에 영향을 미칠지 의아해 한다면 아래 이야기를 계속 읽어보아라.

소프트웨어 개발 업계의 수많은 거장들은 이렇게 생각할 것이다. 전문성은 장인정신 자체에서 나오는 것이다. 이 비유는 특히 그렇다. — 도구. 좋은 장인은 좋은 도구가 필요다. 그러나 "못난 일꾼이 늘 연장 탓한다"이라는 속담이 있다.

크리스토프 고켈은 우리에게 이렇게 상기시켜주었다. "일을 할때 옳바른 연장을 손에 쥐어라" 그러나 적당한 연장이 없다면 우리 스스로 연장을 만들 수 있으며 이 이야기가 바로 우리가 해야할 일이다.

Xcode는 수많은 써드파티 플러그인을 사용했었다. "사용했었다"라고 과거시제를 쓴 이유는 Xcode8부터 더이상 플러그인을 지원하지 않기 때문이다. 이것은 굉장히 슬픈 소식이지만, 좋은 이유(보안상의 이유나 신뢰성과 관련되있는)에서 이렇게 하게 되었다.

애플은 무책임하게 IDE를 플러그인으로부터 벗어내어 개발자를 힘들게 하려는게 아니라, 새로운 방식으로 고유의 툴을 만들 수 있게 제공한다. WWDC에서 새 기능에 대해 멋지게 설명해주었지만 우리 App'n'roll'에서는 우리 손으로 직접 코드를 짜보는게 최고의 방법이라 생각하여 그렇게 해보았다.

JSON Models
많은 개발자들이 앱에서 네트워킹 작업을 할 것이다. 그리고 JSON을 파싱하여 모델로 만드는 작업을 하는데 시간을 소비한다. 이러한 일은 IDE에서 대신 해줄 수 있는 일이므로 우리는 extension으로 이것을 만들어 보기로 했다. 몇 가정을 하고 작업에 갔는데, 첫째로 현재 수정되는 파일은 JSON 타입이여야한다. 둘째로 감쌓여진(nested) 오브젝트에 연관된 가장자리 케이스는 무시한다.

중요 note : 우리 예제는 Xcode8.0 베타2에서 만들어졌고, 아마 항상 모든 버전에서 잘 동작하지는 않을지도 모른다. 만약 여전히 엘케피탄에서 작업하고 있다면 Xcode8.0 beta Release Notes에 들어가서 IDE와 Source Editor Extension에 관한 Xcode8.0 beta 이슈를 보아라. 또한 당신의 extension을 실행시킬때 약간의 딜레이가 있다. 만약 프로젝트가 너무 빨리 열린다면 extension이 불러와지기 전에 켜진것이며, 테스트 Xcode의 인스턴스 메뉴에서 사용할 수 없을 것이다.

이제 새 macOS 프로젝트를 생성하고(UnitTests 박스에 체크했는지 확인해보라) 기본 앱에서 Xcode Source Editor Extension이라는 새 타깃을 추가하여 시작해보자.



이렇게하면 하나의 Info.plist와 두개의 클래스를 자동으로 만들어 줄 것이다. 코드를 자세히 살펴보기 전에 먼저 plist를 보자. Xcode의 메뉴에 보이는 이름을 바꾸기 위해 Bundle Name과 XCSourceEditorCommandName을 고친다.

자동으로 만들어진 첫번째 클래스는 XCSourceEditorExtension이고 이것은 extension이 불러와질때 우리에게 알려주는 역할을 한다. 이번 프로젝트에서는 굳이 손 델 필요가 없다. 두번째 클래스는 XCSourceEditorCommand이다. 명령을 내릴때 실행되는 perform(with invocation:, completionHandler:)  메소드가 하나 프로토콜로서 정의되있을 것이다. 이 extension은 현재 파일의 내용물과 그것을 수정하는 방법을 제공한다. 우리는 간단하게 추상화된 층을 사용하여 유닛테스트하기 쉬운 방향으로 만들어갈 것이다. 파일과 함께 인터렉션 하는 것은 SourceFile 프로토콜을 따른다.

다음 순서는 테스트하기 쉽게 도와주는 오브젝트이다. 이것을 JSONConverter라 부르자.

SourceFile을 받고 뭔가 문제가 생기면 예외로 넘겨주는 메소드 하나만 가지고 있다. XCSourceEditorCommand와 합치려면 아래처럼 간단한 연결점이 필요하다.

아직까지는 extension을 실행하고 명령을 해도 아무일도 일어나지 않을 것이다. 이제 TDD 방식을 조금 사용해보자. 먼저 테스트에 기반한 시스템을 만들고 소스파일을 위한 테스트 쌍을 작성한다.

첫번째 테스트는 유효한 JSON으로 파싱되었는지 체크한다.

다음으로는 한 JSON 양식으로된 문자열을 JSON으로 파싱한다. 여기 테스트가 있다.

...그리고 여기 그 구현이 있다

이제 약간 꼼수를 써서 NSNumber의 서로다른 타입들을 위해 3개의 테스트를 만든다.
...그리고 이것은 한번에 테스트를 통과할 것이다.
다음 이 코드는 런타임동안 오브젝트의 타입을 체크하고 Swift 타입과 일치하게 만들어준다.

다음 우리의 리스트가 한 배열로 만들어지는지의 테스트이다.

The next step is parsing a simple JSON with one String property. Here is the test:

...그리고 구현이다.

마지막으로 감싸진 타입의 파싱 기능은 특별히 우리에게 필요한 것이었다. 이것이 가능한지 알아보는 테스트는 아래 테스트로 충분히 확인할 수 있다.

감싸진 타입은 다른 타입보다 더 많은 구현이 필요하다. 여기 JSONConverter의 완성된 구현이다.

끝이다. 우리는 앞으로 모델 네이밍 시스템을 약간 바꿀 예정이다. JSONConverter는 이미 XCSourceEditorCommand와 합쳐졌고 이제 어떻게 Xcode에서 동작하는지 체크하는 것만 남았다.

첫 Xcode Source Editor Extension을 완성한 것을 축하한다.

요약
툴을 사용하는 것은 모든 소프트웨어 개발자에게 매우 중요한 영역이다. 우리 IDE 프로바이더는 항상 우리가 원하는 것을 제공해주는 것이 아니므로 때론 우리 손으로 직접 그 문제를 해결해야한다. 이런 맥락에서 Xcode Extension은 활용하기 좋은 기능이다.

애플이 이번에 처음으로 발표한 시점이긴 하지만, 우리 개발자에게 AST와 파일 시스템에 접근할 수 있게 해주는 순간 어마어마한 가능성을 가지게 될 것이다. 여기 Github에서 예제코드를 확인해 볼 수 있다.

사용된 모든 이미지는 CC0 1.0 Universal (CC0 1.0)로 쓰였다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

약 4달전, 우리팀(Marco, Arne, and Daniel)은 새 앱의 모델 레이어를 설계하기 시작했다. 우리는 테스트를 개발 과정에서 사용하고 싶었고, 회의를 거쳐 XCTest를 테스트 프레임워크로 정했다.

(테스트도 포함한) 우리의 코드베이스는 190개의 파일과 18,000 라인의 소스로 544KB까지 커져있었다. 우리 테스트에 들어가보면 우리가 테스트할 코드의 2배정도 되는  1,200KB 크기나 된다. 아직 프로젝트가 끝난 상황은 아니지만 거의 마무리 단계에 있다. 이 글을 통해 우리가 무엇을 배웠는지, 일반적인 테스트에 관하여나 XCTest에 관한 주제를 공유하고 싶다.

이 프로젝트는 아직 앱 스토어에 올라가지 않고 진행중이기 때문에 몇몇 클래스 이름이나 메소드 이름은 계속 바뀌어오고 있는 중임을 유의하라.

우리가 XCTest를 고른 이유는 간단하고 Xcode IDE와 잘 결합되기 때문이다. 이 글이 여러분의 XCTest를 고르거나 다른 것을 고를 때 결정을 도와줄 수 있길 바란다.

우리는 이 이슈와 비슷한 주장으로 이어가려 노력했다.

왜 테스트 해야하나
article about bad practices in testing에서 언급했듯, 많은 사람들이 "우리가 코드를 바꿀 때만 테스트 할 가치가 있다"고 생각한다. 이것에 대해 더 명확하게 짚고 싶으면 위 글을 읽어보면 된다. 그러나 사실 첫 버전의 코드를 작성할 때는 코드를 수정하는데 많은 시간이 들 수 밖에 없음을 인지해야한다.—프로젝트가 진행됨에 따라 더 많은 기능들이 추가되며, 그러면 코드 여기저기를 조금씩 수정해야 할 것이다. 따라서 1.1버전이나 2.0버전의 작업이 아니더라도 여전히 수많은 변경할 부분이 있을 것이고, 이때 테스트는 많은 도움을 줄 것이다.

우리는 아직도 최초버전의 프레임워크를 완성하는 과정에 있으며 최근데 10 man months 이상동안 1,000개의 테스트 케이스를 통과시켜 왔다. 우리 프로젝트 아키텍처가 명확한 버전을 가지고 있지만, 여전히 그 방법으로 코드를 수정하고 맞추고 있다. 계속 증가하는 테스트 케이스들은 이렇게 우리를 도와왔다.(원문: The ever-growing set of test cases have helped us do this.)

테스트는 우리 코드의 품질을 안정적으로 만들 수 있게 해주고, 코드를 부수지 않고 리팩토링이나 수정을 할 수 있는 능력을 가지게 해준다. 그리고 모든 코드가 합쳐지지 않아도 매일 코드를 실제 돌려볼 수 있게 해주었다.

XCTest는 어떻게 동작할까
애플은 XCTest 사용하기라는 문서를 제공한다. 테스트는 XCTestCase 클래스의 서브클래스 안에 그룹되어 만들어진다. test로 시작하는 각 메소드들이 실제 테스트이다.

테스트는 간단한 클래스나 메소드이기 때문에, 우리가 원하는 것에 맞춰 @property나 필요한 메소드를 테스트 클래스에 추가할 수 있다.

우리는 코드를 재사용하기 위해 모든 테스트 클래스의 수퍼클래스는 일반적으로 TestCase이다. 이 클래스(TestCase)는 XCTestCase의 서브클래스이다. 모든 테스트 클래스는 TestCase를 수퍼클래스로 한다.

또한 TestCase 안에 다 같이 사용하는 헬퍼 메소드도 하나 넣겠다. 그리고 각 테스트에 필요한 프로퍼티도 넣겠다.(원문: And we even have properties on it that get pre-populated for each test.)

네이밍
test라는 단어로 시작하는 메소드가 하나의 테스트이고, 일반적으로 테스트 메소드는 아래와 같이 생겼다:

우리 모든 테스트들은 "testThatIt"으로 시작한다. 테스트 네이밍에서 자주 쓰는 또 다른 방법은 testHTTPRequest처럼 테스트된 클래스나 메소드 이름을 사용하는 것이다. 그러나 이것은 가볍게 보기만해도 그 테스트의 의미를 바로 알 수 있을 것이다.

"testThatIt" 스타일은 우리가 원하는 결과에 초점이 쏠리고, 대부분의경우 한번에 이해하기 힘들다.

각 제품 코드 클래스의 테스트 클래스가 있고, 어떤 것은 Test가 접미에 붙기도 한다. HTTPRequestHTTPRequestTests클래스가 커지면 이것을 토픽에 따라 카테고리로 쪼개는 작업을 한다.

앞으로 영원히 테스트를 할 필요가 없으면 접두에 DISABLED를 붙인다:

이렇게하면 검색하기도 쉽고, 더이상 메소드 이름이 test로 시작하지도 않음으로 XCTest가 알아서 이 메소드를 생략한다.

Given/When/Then
우리는 모든 테스트를 Given-When-Then으로 나누어 만드는 패턴 구조를 사용한다.

given은 모델 오브젝트들을 만들거나 테스트를 위한 특정 시스템 상태로 만들어 테스트 환경을 셋업하는 영역이다. when은 테스트 하고 싶은 코드를 가지고 있는 영역이다. 대부분 테스트할 메소드 하나를 호출한다. then은 액션의 결과를 확인하는 역역이다. 우리가 기대하던 결과가 나왔는지, 오브젝트가 변경되었는지등을 확인한다. 이 영역은 assertion으로 구성되있다.

아래에 꽤 간단한 테스트가 있다:

이 기본 패턴을 따름으로서 더 짜기쉽고 이해하기 쉽게 해준다. 가독성을 높히기 위해 해당 영역의 상단에 "given", "when", "then"을 주석으로 달아놨다. 이 경우는 테스트된 메소드가 즉시 눈에 띈다.

재사용 가능한 코드
테스트를 여러번 하다보니, 테스트 코드 속에 자꾸 자꾸 재사용되는 코드를 발견했다. 비동기적 처리를 완료할때까지 기다리거나, CoreData 스택을 메모리에 옮기는 그런 코드들을 중복해서 사용하고 있었다. 우리가 최근에 사용하기 시작한 또다른 유용한 패턴은 XCTestCase 클래스에서 직접 프로토콜을 델리게이트하는 것을 구현하는 것이다. 이렇게 하면 엉성하게 델리게이트를 모의객체로 만들지 않고, 꽤 직접적인 방법으로 테스트 할 클래스와 소통할 수 있다.

It turned out that this is not only useful as a collection of utility methods. The test base class can run its own -setUp and -tearDown methods to set up the environment. We use this mostly to initialize Core Data stacks for testing, to reseed our deterministic NSUUID (which is one of those small things that makes debugging a lot easier), and to set up background magic to simplify asynchronous testing. 

Another useful pattern we started using recently is to implement delegate protocols directly in our XCTestCase classes. This way, we don’t have to awkwardly mock the delegate. Instead, we can interact with the tested class in a fairly direct way.

모의객체(Mocking)
우리가 쓰는 모의객체 프레임워크는 OCMock이다. 이 모의객체 주제의 아티클에서 이야기하듯, 모의객체는 메소드 호출에 준비된 결과를 반환하는 오브젝트이다.

우리는 모의객체를 한 오브젝트의 모든 의존성을 위해 사용한다. 이렇게 하면 타깃 클래스를 독립적으로 테스트할 수 있다. 단점이 있다면, 그 클래스에서 뭔가 바뀌게되면 그 클래스에 의존하는 다른 클래스의 유닛 테스트를 자동으로 실패로 만들지 않는다. 그러나 우리는 모든 클래스를 함께 테스트하는 통합 테스트를 하여 이 문제를 해결할 수 있다.

우리는 'over-mock'하지 않도록 주의해야하는데, 이것은 테스트할 하나를 제외한 나머지 모든 오브젝트를 모의객체로 만드는 것이다. 우리가 처음 시작할 때 이런 방식으로 테스트 했었고, 심지어 메소드에 입력하기위해 사용된 간단한 오브젝트까지도 모의객체로 만들었다. 이제는 많은 오브젝트를 모의객체 없이 사용하는 방법으로 테스트 하고 있다.

모든 테스트 클래스를 위한 우리 일반적인 슈퍼클래스의 일부이고, 한 메소드를 추가한다. 
이것은 메소드/테스트 마지막에서 검증하는 모의객체이다. 이것이 모의객체 사용을 더욱 편리하게 만든다. 우리가 만든 모의객체가 그 지점에 옳바르게 있는지 확인할 수 있다:

상태와 상태없음(State and Stateless)
지난 몇년동안 상태없는 코드를 많이 이야기해왔다. 그러나 결국 우리 앱은 상태를 필요로 했다. 상태가 없는 앱은 꽤 요점을 잃어버린다. 반대로 상태를 관리하면 그것이 굉장히 복잡하기 때문에 수많은 버그를 만들어 내기도 한다.

우리는 상태로부터 코드를 떼어내어 작업하기 쉽게 만들었다. 몇몇 클래스는 상태를 가지고 있으나 대부분의 클래스에는 상태가 없다. 또한 코드를 테스트하기도 아주 쉬워졌다.

예를들어 우리가 EventSync라는 클래스가 있는데, 이 클래스의 역할은 로컬의 변화를 서버에 보내는 것이다. 이것은 어떤 오브젝트가 서버에 갱신을 보내야하는지 현재 서버에 보내진 갱신들은 무엇인지 기억하고 있어야한다. 한번에 여러 갱신을 보낼 수 있지만 같은 갱신을 두번 보내서는 안된다.

또한 우리가 주시해야하는 오브젝트들 사이는 상호의존적이다. 만약 A가 B에 연관되있고 B에서 로컬 갱신이 일어나면, A 갱신을 보내기 전에 B 갱신을 먼저 서버에 보낼때까지 기다려 주어야 한다.

우리는 다음 요청을 만드는 -nextRequest 메소드를 가진 UserSyncStrategy를 가지고 있다. 이 요청은 로컬에서의 갱신을 서버로 보낼 것이다. 이 클래스 안에는 상태가 없으나 그 모든 상태는 UpstreamObjectSync 클래스 안에 캡슐화되어 들어 있는데, 이 클래스는 유저가 만든 모든 로컬 갱신에 대한 기록을 서버에 날린다. 이 클래스 바깥에는 상태가 없다.

이 경우 이 클래스가 관리하는 상태가 올바른지 체크한다. UserSyncStrategy의 경우 UserSyncStrategy를 모의객체로 만들어 UserSyncStrategy 내부의 상태에 더이상 신경 쓰지 않아도 된다. 이것이 테스트의 복잡도를 확 낮춰주는데, 수많은 다른 종류의 오브젝트를 동기화하고 있기 때문이다. 그러면 다른 클래스들은 상태가 없으며, UpstreamObjectSync 클래스를 재사용 할 수 있다.

Core Data
우리 코드는 굉장히 Core Data에 의존한다. 우리 테스트가 다른 하나로부터 독립되야하므로 각 테스트 케이스마다 명확한 Core Data 스택을 만들어야하고 그 후에 그것을 다시 원래대로 해야했다. 우리는 이 store를 한 테스트 케이스에만 사용하고 다음 테스트에는 다시 사용하면 안되었다.

우리 모든 코드는 다음 두가지 Managed Object Context 주변에 집중되있다: 하나는 유저 인터페이스가 사용하고 메인 큐에 묶여있는 것이고 다른 하나는 동기화를 위해 사용되며 자신의 개인 큐를 가지고 있다.

우리는 그들이 필요로하는 모든 테스트마다 Managed Object Context 자꾸자꾸 생성하길 원하지 않는다. 그러므로 공유된 TestCase 수퍼클래스의 -setUp 메소드에 두개의 Managed Object Context를 만들어둔다. 이것은 각 개별 테스트에서 가독성을 높혀준다.

Managed Object Context가 필요한 테스트는 간단하게 self.managedObjectContextself.syncManagedObjectContext를 호출하면 된다:

우리는 코드의 일관성을 만들기 위해 NSMainQueueConcurrencyTypeNSPrivateQueueConcurrencyType을 사용하고 있다. 그러나 독립적 문제 때문에 -performBlock: 상단에 우리만의 -performGroupedBlock:을 구현했다. 이것에 대핸 더 많은 자료는 비동기 코드를 테스팅하는 섹션에서 볼 수 있다. 

여러 컨텍스트를 합치기
우리 코드에는 두 컨텍스트를 가지고 있다. 프로덕션에는 -mergeChangesFromContextDidSaveNotification:의 의미로서 한 컨텍스트가 다른 컨텍스트와 합쳐지는 것에 굉장히 의존적이다. 우리는 동시에 각 컨텍스트 별로 독립된 퍼시스턴스 store coordinator를 사용하고 있다. 그러면 두 컨텍스트 모두 최소의 명령으로 한 SQLite store에 접근할 수 있기 때문이다.

그러나 테스트를 위해 약간 바꾸어서 메모리 store를 사용할 것이다.

테스트시 SQLite store를 사용하여 디스크에 두는 것은 디스크 store에서 삭제시 경쟁상태(race condition)를 만들기 때문에 동작하지 않는다. 이것은 테스트간의 독립성을 해칠 것이다. 반면 메모리에 store하면 매우 빠르게 동작하며 테스트하기도 좋다.

우리는 모든 NSManagedObjectContext 객체를 만들기 위해 팩토리 메소드를 사용한다. 기본 테스트 클래스는 이 팩토리 클래스를 약간 고쳐 모든 컨텍스트가 같은 NSPersistentStoreCoordinator를 공유한다. 각 테스트의 마지막에는 다음 테스트가 사용할 새 것이나 새 store가 있는지 확인하기 위해 공유하고 있던 퍼시스턴트 store coordinator를 버린다.

비동기적 코드를 테스트하기
비동기적인 코드는 조금 까다로울 수 있다. 그러나 대부분 테스트 프레임워크는 비동기적 코드를 위한 기본 기능을 지원한다.

NSString에 비동기적인 메시지를 가지고 있다고 해보자:

XCTest에서는 아래와 같이 테스트할 수 있다:
대부분 테스트 프레임워크가 이런식으로 되었다.

그러나 비동기 테스트의 주된 문제는 독립적으로 테스트 하기 힘들다는 것이다. 테스트 습관에 관한 글에서 말했듯, 독립(Isolation)의 첫 글자는 "I" 이다.(원문: Isolation is the “I“ in FIRST, as mentioned by the article about testing practices.)

비동기 코드에서 다음 테스트가 시작하기 전에, 현재 테스트의 모든 스레드와 큐가 완전히 멈추는 것을 확신하기 까다로울 수 있다.

이 문제에 대해 우리가 찾는 최고의 해결책은 dispatch_group_t라는 이름의 그룹을 사용하는 것이다.

혼자 두지 말고 그룹에 넣자
몇 우리 클래스들은 내부적으로 dispatch_queue_t를 사용할 필요가 있다. 몇 우리 클래스들은 NSManagedObjectContext의 private 큐에 블럭들을 넣는다.

모든 비동기 작업이 끝날때까지 -tearDown 메소드에서 기다린다. 이것을 하기위해 우리는 아래 보이는 것처럼 여러 일들을 한다.

테스트 클래스는 이런 프로퍼티를 가진다:

우리는 이것을 일반적인 수퍼클래스에 한번만 선언해 두었다.

다음으로 dispatch_queue나 그 비슷한 것을 사용하는 모든 클래스 안에 이 그룹을 넣었다. 예를들어 dispatch_async()를 호출하는 대신, dispatch_group_async()를 호출하였다.

우리 코드는 CoreData에 의존적이므로 NSManagedObjectContext에 호출하는 메소드도 추가하고
모든 Managed Object Context에 새 dispatchGroup 프로퍼티를 추가했다. 그래서 우리는 독립적으로 -performGroupedBlock:을 사용했다.

이렇게하여 모든 비동기 처리가 끝날때까지 tearDown 메소드에서 기다릴 수 있었다.

메인 루프에서 -tearDown이 호출된다. 메인루프에서 큐에 들어간 어떤 코드가 실행되었는지 확인하기 위해 메인루프를 끈다. 위 코드는 그룹이 비지 않는한 영원히 돌고 있다. 우리의 경우 타임아웃을 넣어 살짝 바꾸었다.

모든 작업이 끝날때까지 기다리기
이렇게 하면 수많은 다른 테스트들도 쉬워진다. 아래와 같이 사용할 WaitForAllGroupsToBeEmpty()를 만들었다:

마지막 라인은 모든 비동기 작업이 완료될때까지 기다리는 코드이다. 즉, 이 테스트는 추가적인 비동기 처리를 큐에 넣은 비동기 블럭들까지도 모두 끝나고 어떠한 것도 거절된 메소드를 호출하지 않는다.

이것을 만든한 메크로로 만들었고:
나중에는 공유된 TestCase 수퍼클래스에 메소드를 정의했다:

커스텀 예외
이 섹션의 초반부에서, 어떻게 이것을 하는지 이야기 했었고

비동기 테스트를 위해나 블럭을 만드는 기본이다.

XCTest는 NSNotification과 key-value observing을 위한 몇가지 약속이 존재하는데, 이 둘다 블럭을 만드는 최상단에서 구현될 것이다.

그러나 종종 여러 곳에서 이 패턴을 사용하고 있다는 것을 발견하였다. 예를들어 Managed Object Context가 비동기적으로 저장될거라 예상할 때, 우리 코드는 이렇게 생길 것이다:

이 코드를 공유된 한 메소드만을 호출하게하여 가볍게 만들었다:

그리고 테스트에서 사용할 때이다:

이렇게하면 가독성이 더 좋아진다. 이 패턴은 사용하면 다른 상황에서도 자신만의 커스텀 메소드를 추가할 수 있다.

The Ol’ Switcheroo — Faking the Transport Layer
앱을 테스트하는데 중요한 줄문 중 하나는, 어떻게 서버와 연동하여 테스트할 것인지 이다. 가장 이상적인 솔루션은 실서버를 로컬에 빨리 복사하고, 가짜 데이터를 제공하여 http를 통해 직접 테스트를 돌려보는 것이다.

사실 우리는 이러한 솔루션을 이미 사용하고 있다. 이 솔루션은 굉장히 실제와 유사한 테스트 환경을 제공한다. 그러나 현실적으로 너무 느리게 환경설정이 된다. 각 테스트마다 서버의 DB를 정리하는 것이 너무 느리다. 우리는 1000여개의 테스트를 가지고 있다. 실서버에 의존하는 30개의 테스트가 있는데, 만약 DB를 정리하고 서버 인스턴스를 깨끗히 만드는데 5초가 걸린다치면 적어도 2분 30초를 테스트를위해 기다려야 한다는 것이다. 그리고 또한 서버 API가 구현되기 전에 서버 API를 테스트할 수 있는 것도 필요했다. 우리는 뭔가 다른 것이 필요했다.

이 대안의 솔루션은 '가짜 서버(fake server)'이다. 우리는 서버와 통신하는 모든 클래스를 TransprotSession이라는 한 클래스와 통신하도록 구조를 짜고, 이 클래스는 NSURLSession과 비슷한 스타일이지만 JSON 변환까지도 처리해준다.

우리는 UI에 제공할 API 테스트들을 가지고, 서버와 통신하는 모든 것들은 TransportSession이라는 가짜 서버로 우회하여 두었다. 이 transport session은 실제 TransportSession과 서버 모두의 행동을 따라한다. 이 가짜 session은 TransportSession의 모든 프로토콜을 구현하여 그것의상태를 설정할 수 있게 해주는 몇 메소드를 추가한다.

OCMock를 사용하여 각 테스트에 커스텀 클래스를 가지는 것은 모의 서버(mocking the server)를 넘어 여러 이점을 가진다. 그중 하나는, 실질적으로 모의 서버를 사용하여 더 복잡한 시나리오를 만들어 테스트해볼 수 있다. 실제 서버에서는 시도해보기 어려운 극한의 상황을 시뮬레이트 해볼 수 있다.

또한 가짜 서버는 그 스스로 테스트를 가지므로 그 결과가 좀 더 정밀하게 정의되어 있다. 만약 요청에 대한 서버의 응답이 항상 바뀌어야 한다면 한 장소에서 오직 그렇게 한다. 이것은 가짜 서버를 사용하는 모든 테스트를 보다 더 튼튼하게 만들며, 우리 코드에서 새 기능이 잘 동작하지 않는 부분을 좀 더 쉽게 찾아낼 수 있다.

FakeTransportSession 구현은 간단하다. HTTPRequest 객체를 요청에 관한 URL, 메소드, 패이로드(payload)에 관련하여 캡슐화 시키면 된다. FakeTransportSession은 내부 메소드에 모든 끝부분을 매핑시키고 응답을 발생시킨다. 이것이 알고있는 오브젝트의 기록을 가지고 있기 위해 메모리에 담은 CoreData 스택까지도 가지고 있는다. 이렇게하여 PUT으로 추가된 이전 오퍼레이션의 리소스를 GET으로 반환할 수 있다.

이 모든것을 하기에 시간이 부족하다고 생각할 수도 있겠지만 사실 가짜 서버는 꽤 간단하다. 실제 서버가 아니며, 많은 부분을 떼어냈다. 가짜 서버는 오직 한 클라이언트에만 기능을 제공하기 때문에 퍼포먼스나 스케일리비티는 전혀 신경쓰지 않는다. 또한 한번에 큰 노력을 들여 모든것을 구현할 필요가 없다. 우리가 개발이나 테스트에 필요한 부분만 만들면 된다.

그러나 우리 상황의 경우, 우리 테스트를 시작할쯤엔 서버 API가 꽤 안정적이고 잘 정의되있었다.

커스텀 Assert 메크로
Xcode 테스트 프레임워크에선, 실제 확인을 위해 XCTAssert 메크로를 사용한다:
애플의 “Writing Test Classes and Methods” 글에 "카테고리로 정의된 Assertion의 모든 목록이 있다.

그러나 우리는 아래와 같은 특정 도메인을 체크하는 Assertion을 자주 사용했다:
이렇게하면 가독성이 너무 떨어지고, 코드의 중복을 피하기 위해 간단한 assert 메크로를 만들었다:

테스트 할때는 아래와같이 간단하게 사용하면 된다:

이 방법으로 테스트의 가독성이 굉장히 좋아졌다.

한단계 더
그러나 우리 모두가 알듯 C의 전처리기 메크로는 굉장히 난잡하다(a beast to dance with).

몇몇은 이것을 피할 수 없으며 그 고통을 줄이고싶은 것에 대한 이야기이다. 어디라인 어디파일에 assertion 실패가 생겼는지 알기 위해 테스트 프레임워크를 정렬하는 경우 메크로가 필요하다.(We need to use macros in this case in order for the test framework to know on which line and in which file the assertion failed.) XCTFail()은 메크로이고 __FILE____LINE__이 설정되는 것에 의존하고 있다.

좀 더 복잡한 assert와 체크를 위해 FailureRecorder라 불리는 간단한 클래스를 만들었다:

우리 코드에는 두 딕셔너리가 서로 일치하는지 확인해야하는 부분이 곳곳에 있는데, XCTAssertEqualObject()가 그것을 체크한다. 이것이 실패했을때 내뱉는 결과가 아주 유용하다.

우리는 이런식으로 하길 원했다:

결과에는

그래서 이렇게 메소드를 만들었다.

FailureRecord가  __FILE__, __LINE__, 테스트 케이스를 잡아내는 방법을 썼다. -recordFailure: 메소드는 그냥 간단하게 문자열을 테스트 케이스로 전달한다:

Xcode, Xcode 서버와 통합
XCTest의 최고 장점은 놀라울 정도로 Xcode IDE와 통합하기 좋다는 것이다. Xcode6과 Xcode6 서버와 함게 작업하면 더욱 빛을 발한다. 이 강력한 결합력은 생산성을 증진시키는데에도 큰 몫을 한다.

초점
테스트 클래스에서 한 테스트나 여러 테스트를 하고 있을 동안, 왼편 라인 넘버 옆에 있는 작은 다이아몬드는 특정 테스트나 테스트 집합을 실행시켜준다.



테스트에 실패하면 빨간색으로 되고:



성공하면 초록색이 된다:




^⌥⌘G 단축키는 마지막 테스트를 다시 돌려볼 수 있게 해주는데, 자주 사용하게 될 것이다. 다이아몬드를 클릭하고, 우리가 테스트를 변경하면, 키보드에 손 델 필요없이 간편하게 다시 테스트를 돌려볼 수 있다. 디버깅 테스트시 아주 유용하다.

네비게이터
(Xcode 왼쪽 창에 있는) 네비게이터는 Test Navigator라는 것인데, 클래스별로 모든 테스트를 묶어 보여준다:



그룹 테스트나 개별 테스트는 이 UI로부터 시작할 수도 있다. 더 유용한 점은 네비게이터 하단의 세번째 아이콘을 활성화시켜 실패한 테스트만 보여주게도 할 수 있다:




이어지는 통합
OSX 서버는 Xcode 서버라 불리기도 한다. 이것은 Xcode를 기반으로 이어지는 통합(continuous integration) 서버이다. 우리는 이렇게 사용해 왔다.

우리의 Xcode 서버는 github의 새 커밋이 들어올때 자동적으로 프로젝트를 체크한다. 우리는 스태틱 어널라이저를 실행하고 iPod touch나 다른 iOS 시뮬레이터에서 모든 테스트를 돌린 뒤 마지막으로 다운받을 수 있는 Xcode 아키브(archive)를 생성하도록 했다.

Xcode6에서는 Xcode 서버의 이 기능들이 복잡한 프로젝트에까지도 꽤 유용하게 쓰인다. 우리는 커스텀 트리거를 가지고 있는데, 이것은 배포 브런치에서 Xcode 서버의 빌드 부분을 실행한다. 이 트리거 스크립트는 생성된 Xcode 아키브를 파일 서버에다 올려둔다. 이렇게 함으로서 버전별 아키브를 관리할 수 있다. UI팀은 파일 서버로부터 미리 컴파일된 특정 버전의  프레임워크를 내려받을 수 있다.


BDD와 XCTest
당신이 만약 BDD(behavior-driven development)에 익숙하다면, 우리의 네이밍 스타일이 이 방식(BDD)에 영감을 받았다는 것을 알 수 있을 것이다. 우리 중 몇명은 Kiwi라는 테스트 라이브러리를 사용해 보았고 자연스럽게 클래스나 메소드의 동작에 집중함을 느꼈을 것이다. 그러면 XCTest가 그 좋은 BDD 라이브러리를 대체할 수 있을까? 대답은 아니오 이다.

XCTest가 간편하다는 것에는 장단점이 분명 존재한다. 당신이 클래스를 생성하고 "test"라는 단어를 접두에 붙인 테스트 메소드를 만들어서 그렇게 해도 된다. 게다가 Xcode와 XCTest는 최고의 통합을 자랑한다. 한 테스트를 실행하기 위해 왼편의 다이아몬드를 누르면 되고, 실패한 테스트들을 ㅅ ㅟㅂ게 걸러볼 수 있으며, 또한 테스트의 리스트 중에 원하는 테스트로 쉽게 이동할 수도 있다.

불행히도 당신에게 이런것들을 새로 배우기에 꽤 부담스러울 수 있는 양이다. 우리는 XCTest와 개발/테스트하면서 어떠한 장애물도 만나지 않았으나 종종 더 편하게 사용해왔다. XCTest 클래스는 일반 클래스처럼 보이지만, BDD 테스트 구조는 nested context가 있다. 이것은 테스트시 nested context를 만드는 것을 잊어버릴 수도 있다. nested context는 개별 테스트를 간단하게 하면 더 많은 특정 시나리오를 만들어내야한다. 물론 XCTest에서도 그렇긴하다. 예를들어 몇 테스트를 위해 커스텀된 초기화 메소드를 호출함우로써 말이다. 이것의 단지 편리함 때문만은 아니다.

BDD 프레임워크의 추가적인 기능이 얼마나 중요한지는 프로젝트의 크기에 따라 알게될 것이다. 우리의 결로은 다음과 같다. XCTest는 작은 사이즈나 중간 사이즈의 프로젝트에 적합하나, 큰 사이즈의 프로젝트에는 KiwiSpecta 같은 BDD 프레임워크를 사용하는 것이 더 낫다.

요약

XCTest가 옳바른 선택일까? 당신의 프로젝트에 따라 판단해야한다. 우리는 KISS의 부분으로 XCTest를 선택했고—다르게 해보고 싶었던 위시리스트를 가진다. XCTest는 우리가 어느정도 절충해야 하지만, 그 역할을 잘 한다. 그러나 다른 프레임워크에서는 다른 것들도 절충해야할 것이다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


요즘 앱에서 멀티 스레딩이나 컨커런시(concurrency)는 거의 필수이다.. 그리고 동시성을 관리해주는 시스템단 라이브러리인 Grand Central Dispatch는 iOS SDK에서 아직까진 다루기 까다롭고 친숙하지 않은 API를 제공해왔었다.

하지만 더이상은 아니다.

Swift3에서 Grand Central Dispatch의 문법과 사용법이 많이 개선 되었다. 이 글은 그것의 새로운 사용법을 빠르게 훑어볼 것이다.

dispatch_async
이전에는 dispatch 메소드(동기적이든 비동기적이든)를 고른 뒤, 우리가 dispatch 하고싶은 작업을 큐에 넣었다. 새로 바뀐 GCD는 이 순서가 반대이다.  ― 큐를 먼저 고른 뒤에 dispatch 메소드를 적용한다.

일반적인 GCD 패턴은 글로벌 백그라운드 큐에서 작업을 수행하고, 작업이 끝나는대로 메인 큐에서 UI를 갱신하는 방식이다. 아래 코드는 새 API의 모습이다.


큐 속성들
이제 큐가 초기화 시점에서 속성을 받는다. 이것이 Swift OptionSet이며, 순차적vs동시, 메모리, 엑티비티 관리 옵션과 서비스 품질(.default, .userInteractive, .userInitiated, .utility and .background)과 같은 큐 옵션을 설정할 수 있다.

서비스의 품질(The quality of service)은 앞서 iOS8부터 디프리케이트(deprecated)된 이전 속성 대신으로 사용된다. 만약 예전 방식으로 큐를 사용하고 있었다면, 어떻게 바뀌었는지 확인해보기 바란다.

* DISPATCH_QUEUE_PRIORITY_HIGH: .userInitiated
* DISPATCH_QUEUE_PRIORITY_DEFAULT: .default
* DISPATCH_QUEUE_PRIORITY_LOW: .utility
* DISPATCH_QUEUE_PRIORITY_BACKGROUND: .background



메모리 엑티비티 관리 옵션은 올해(2016년) 애플OS를 릴리즈하면서 새로 나왔다. .initiallyInactive를 사용하여 비활성 상태에서 큐를 시작하게 한다던지, .autoreleaseInherit, .autoreleaseNever and .autoreleaseWorkItem를 사용해여 커스텀된 오토릴리즈를 설정할 수 있다.

Work items
큐들은 GCD에서만 Swift OptionSet을 필요로 하는게 아니다. 새로 바뀐 work item의 Swift 문법에서도 쓰인다.

한 work item은 이제 퀄리티나 서비스를 정의하고 (혹은) 초기화때 flags를 줄 수 있다. 이 둘다 선택적으로 가능하며 work item 실행시 영향을 준다. flags는 barrier, detached, assignCurrentContext, noQoS, inheritQoS, enforceQoS 옵션들이다.

dispatch_once
dispatch_once는 한번만 실해오디는 코드나 함수들을 초기화하는데 매우 유용했다.

Swift3에서는  dispatch_once가 디프리케이트 되었고 이것은 글로벌, 스태틱 변수와 상수를 사용하는 것으로 대체되었다.


dispatch_time_t
dispatch_time_t는 큐에서 사용할 수 있는 UInt64로 특정 시간을 변환하는 함수이다. 새로 바뀐 GCD는 이것에대해 좀 더 친숙한 문법을 소개했다.(NSEC_PER_SEC여 안녕!) 아래 코드는 바뀐 dispatch의 예제이다:

.second는 DispatchTimeInterval에서 불려진 새 열겨형 중 하나이다. 이 열겨형은 카운트를 표현하기위한 값들을 갖는다. 현재 지원하는 것들이다:
* .seconds(Int)
* .milliseconds(Int)
* .microseconds(Int)
* .nanoseconds(Int)


dispatch_assert
또한 이번에 새로 발표한 애플OS에서는 dispatch precondition이 있다. 이것은 dispatch_assert를 대체하며, 이것은 코드가 실행되기 전에 스레드를 생각하고 있는지 아닌지 체크할 수 있다. 이것은 특히 UI를 업데이트하고 메인 큐에서 반드시 실행되야할 함수에 유용하게 쓰인다. 예제코드를 한번보자:


추가적인 자료들

여기 Swift3을 포함한 더 많은 GCD 개선에 관한 이야기들이 있지만, 아직 공식적인 문서는 완성되지 않고 있다. 더 심화된 자료들이다:
  • https://github.com/apple/swift-evolution/blob/master/proposals/0088-libdispatch-for-swift3.md
  • https://developer.apple.com/videos/play/wwdc2016/720/
  • https://github.com/apple/swift-corelibs-libdispatch



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원문 : medium.com/@mandrigin/ios-app-performance-instruments-beyond-48fe7b7cdf2?source=userActivityShare-d07a45aa48c6-1455001286


iOS 퍼포먼스: Instrument 이상

유저들은 기다리는걸 굉장히 싫어한다. 그들은 앱이 뭔가 초기화 한다는것을 전혀 모른 상태에서 자신들의 업무 처리를 최대한 빠르게 하고싶어한다. 그러므로 앱이 모두 즉각적으로 시작할 있다면 인터페이스가 흐르듯 부드럽게 넘어갈 있다. 퍼포먼스가 나오는 앱은 소프트웨어 마켓에서 경쟁력있는 특징중 하나이다. 개발자로서 앱이 퍼포먼스있게 동작하게 하는것을 자랑스러워하는 것을 원하는것도 이유이다.

그러나 많은 이들이 겪듯 퍼포먼스 최적화는 다루기 어려운 문제이다. 대부분 문제를 직관적으로 접근하기 어렵다. 각기 정리된 측정법 없이는 앱이 느려지는 이유를 알아내는건 매우 어렵다.

당신의 퍼포먼스를 최적화하기 위해서는 데이터에 기반한 결론을 내려야한다. 장에서는 당신의 앱에 서로 다른 부분에서 퍼포먼스 측정 데이터를 어떻게 얻어낼 있는지 보여줄것이다.

파트에서는 아래의 것들을 다룰 것이다.

  • 앱에서의 CPU, GPU, 메모리&베터리 사용량
  • 반응성(리스폰시브니스)
  • 앱시작동안의 시간
  • 유저로부터 퍼포먼스 데이터를 얻는

바로 시작해보자!

CPU, GPU, 메모리&베터리 사용량

첫번째 할일은 CPU, GPU, 메모리를 과하게 사용하는 비효율적인 코드를 찾아내는 일이다. 애플은 일을 하기위한 좋은 (Instruments) 제공한다.

우리가 주로 측정해야할 부분은 아래 4 정도이다.

  • CPU ("Time Profiler" 툴 이용)
  • GPU (“Core Animation” 툴 이용)
  • 메모리 사용량 (“Allocations” 툴 이용)
  • 베터리 소모량 (“Energy diagnostics” 툴 이용)

WWDC 비디오는 당신 앱을 분석하기위한 최고의 정보를 제공한다.
아래는 시작하면서 몇 개 골라보았다:


반응성(Responsiveness)

퍼포먼스 측정(이해 "측정"으로 줄여서 말하겠습니다)에 있어서 다음으로 중요한 것은 UI 반응성이다. 터치 헨들링은 메인 쓰레드에서 발생한다. 메인쓰레드에서 시간이 걸리는 작업을 하면, 앱은 버벅거리게 될것이다.

몇몇 동작은 CPU 사용하지 않는 주제에 시간을 잡아먹기도 한다. 만약 메인쓰레드에서 동기화 콜을 불렀다면, 콜이 얼마나 시간이 걸리는지 알아내는것이 문제를 해결하는 방법일것이다.

시간을 측정하기 위해 로그를 찍어볼 수도 있다.

한가지 다른 방법은 Viber 개발자들이 만든 솔루션으로 나타내는 것이다. 솔루션은 메인 쓰레드 하나를 400ms보다 많이 멈추지 않게 지켜보고, 체크하는 특별한 스레드를 가지고있다.

Testing Responsiveness (from Viber’s presentation at NSSpain)


Testing Responsiveness (from Viber’s presentation at NSSpain)


많은 정보는 발표자료(PDF, 7MB)에서 확인할 있다.

데이터를 이용하여 너무 많은 시간이 걸린 (메인싸레드가 멈추는데에는 400ms정도가 적당한 최대치이며, 보면 많은 정보를 얻을 있다.) 찾아내고, 이것을 최적화 시킬지 메인쓰레드 밖으로 보내던지 해야한다.

시작 시간

다음으로 중요한 측정은 앱의 시작하는데 걸리는 시간이다. 전형적인 유저는 당신의 앱을 오직 몇분만 사용한다. 시작시간은 앱의 이미지에 좋지않은 영향을 준다.
여기 시작의 2가지 경우가 있다.

  • Cold 시작 : 당신의 프로세스가 동작하지 않고, OS 의해 실행된다.
  • Warm 시작 : 당신의 앱은 최소화되나 죽지않는다. 이것은 백그라운드로부터 다시 불러온다.


색션에서는 리소스를 많이 잡아먹는 Cold 시작에 초점을 맞출것이다
아래에 iOS 앱의 시작 순서가 나와있다.


The Application Startup Phases (from the documentation)

1. 시작하는데 걸리는 시간을 측정한다.

우리는 main()에서부터 applicationDidBecomeActive:까지 시간이 얼마나 걸리는지 측정해야한다.

앱의 기능을 보여주면서 시간을 잡아먹게 하지 말아야한다. cold 시작 시간을 1 미만으로 떨어뜨리려고 노력하는 것이 좋다.

2. 시작하는 순서에서 부분별로 측정한다.

보통 시작시간의 전체만 아는 것은 충분하지않다. 어떤 부분에서 시작시간을 느리게 만들었는지 아는것 또한 중요하다.
밑에 보이는 것들이 가장 중요한 부분들이다

  • -[AppDelegate application:didFinishLaunchingWithOptions:] - 콜백은 런칭이미지(혹은 스토리보드) 보여질때 호출된다. 곧바로 메소드로부터 return되면 실제 UI 로딩을 시작한다.
  • -[UIViewController loadView] - 앱에 커스텀 뷰를 불러와야한다면, 여기서 뷰를 초기화하게된다.
  • -[UIViewController viewDidLoad] - 뷰가 불러와졌고, 마지막 초기화의 시간이다.
  • -[AppDelegate applicationDidBecomeActivate:] - UI 이미 초기화되어있지만, 콜백이 끝날때까지 블럭되있다. 메소드는 백그라운드로부터 restore될때 호출된다.

몇몇 매소드가 너무 시간이 많이 걸린다면, 최적화시켜야한다.

3. “under pressure” 시작 시간을 측정한다.

전형적으로 현실과 테스트 환경은 다르다는걸 알아야한다.

당신의 앱은 안타깝게도 "현실세계"에서 작동된다. 유저는 종종 다른 앱에서 당신의 앱을 열기도 한다. “다른 굉장히 무거운 앱일 수도 있다. 당신의 앱이 시작될때 다른 무거운 앱은 백그라운드로 가면서 데이터를 저장하려고 있으며, 그런 상황에서의 시간 측정은 굉장히 중요하다.

그런 테스팅(무거운 다른 앱에서 내 앱을 열어보는)을 통해 예측하지 못한 결과를 만들 수도 있기 때문에, 이전에는 코드가 완벽하게 안정적이다가도, 저런 상황에서는 느려질 수도 있다는걸 명심해야한다.

4. 앱이 이미 시작되었지만 여전히 소용없다.(무슨 의미인지 모르겠습니다)

만약 당신 앱이 곧바로 UI 불러오는것이 무의미한 일이라면, 런칭화면이 끝나지 않을 것이다. 비록 UI 불러와지고 반응이 왔었어도, 불러오는데 준비를 위한 데이터가 필요하며, 또한 역시 시작 시간을 재어봐라.

당신의 유저로부터 측정값을 수집해야한다.

모든 측정은 테스트 환경에서 가능할 것이다. 그것은 반드시 필요하지만 너무 완벽하게까지 할필욘 없다. 만약 당신 앱이 인기있는 앱이라면, 만약 유저들이 해외시장을 기반으로 한다면, 몇몇 유저들은 당신이 예상하지 못한 아주다른 환경을 사용할 수도 있다.

추측하기에 다를 있는 환경들이다.

  • 네트워크 상태
  • 하드웨어
  • 소프트웨어(OS 버전, Jailbreak(탈옥...)
  • 기기의 남은 용량
  • 기타 등등

분명 당신 개발실에서 측정한 것은 모두 안정적인 상태일지라도, 별점하나로 컴플레인("앱이 너무 느림ㅡㅡ") 리뷰를 받을 수도 있다. 어떻게 대처하면 될까?
퍼포먼스 측정의 집합을 정의하고(혹은 KPI) 실유저로부터 얻어와야한다. 이것은 대부분 통계 패키지와 함께 사용할 있다.

아래는 당신의 유저로부터 얻을 있는 KPI 예시들이다.

  1. cold 시작시간
  2. warm 시작시간
  3. 마디별 시작시간
  4. 반드시 서버로부터 다운받아야할 것들의 소요시간
  5. 메인쓰레드가 400ms보다 오래 블럭되는
  6. 메모리 워닝이 일어나는
  7. FOOMS(링크)
  8. UI 블럭되거나 쓰잘때기없는 동작의 길이 

결론

쉽게 설명하자면 퍼포먼스 측정은 Instruments.app 프로그램을 열면서 시작하며, 앞에서 본 것 말고도 다양한게 있다. 몇몇 소개된 방법은 구현하기 쉽지만 어떤건 시간과 노력이 필요할 것이다. 어쨋든 솔루션들은 당신의 퍼포먼스 이슈를 찾고 해결을 위한 모니터링하는데 도움을 줌과 동시에 즐겁게 만들것이다.

별점5 리뷰를 받기를 바란다!


+) 이해하면서 의역을 부분이 많이 있습니다. 잘못 이해한 부분이 있다면 지적해주시면 감사합니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

invalid Toolchain. New apps and app updates must be built with the public (GM) versions of Xcode 6 or later, and iOS 8 SDK or later. Don't submit apps built with beta software.

앱 개발을 완료하고 앱 검수를 위해 앱을 업로드한다. 그다음 "심사를 위해 제출"을 누를 때 위와 같은 오류가 뜬다면 나의 경우 아래와 같이 해결했다.

원문 : http://stackoverflow.com/questions/32174954/submitting-app-from-building-in-xcode-6-4/32233429#32233429

Apps that you submit should be developed using the latest version of Xcode from the Mac App Store and should be built for publicly available versions of iOS, OS X, and watchOS — except when GM seeds are available. Now Mac App Store's Xcode is 6.4 and OS X Yosemite is Build 14F27. If you user xcode 6.4 on OS X El Capitan, you should follow the steps:

  1. Using Xcode, then archive your project
  2. Open organizer, find your .xcarchive file find .xcarchive file
  3. Right click the xcarchive file, choose [Show package Contents]
  4. Find Products/Applications/XXX.app/Info.plist
  5. then change [BuildMachineOSBuild] value to 14F27, just like this: example
  6. Now, you can go to Xcode->organizer, then 【Submit to App Store】




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

이번에 두번째 계산기를 개발하다가 만들게된 콤마찍기 알고리즘(?)입니다. 구상하기는 생각보다 복잡했고 시간이 많이 걸렸는데 짜는데 시간은 얼마 안걸렸네요.. (허망) 
아무튼 짠다고 고생했는데 혼자 썩히기엔 아까워서 공유합니다.

사용법

  • 기본적으로 convertCommaFormula: 를 사용하면 문자열을 반환해줍니다.
  • 추가적으로 convertCommaFormula:location: 을 사용하면 현재 커서 위치를 파라미터로 넣고 현재 커서 위치까지 반환(NSDictionary)해 줍니다. 
저는 convertCommaFormula:location: 메서드를 구현해서 사용했었고 편의를 위해 convertCommaFormula: 를 뽑아보았습니다.

NSString *text = @"482193423+1284+41-(327123.4+2)";
NSString *resultText = [NSString convertCommaFormula:text];
NSLog(@"result : %@", resultText); 
// result : 482,193,423+1,284+41-(327,123.4+2)


코드

NSString+ConvertComma.h

#define COMMA_INDEX 3

@interface NSString (ConvertComma)

+ (NSString *) convertCommaFormula:(NSString *)formula ;
+ (NSDictionary *) convertCommaFormula:(NSString *)formula cursorlocation:(NSInteger)location ;

@end


NSString+ConvertComma.m

@implementation NSString (ConvertComma)


+ (NSString *) convertCommaFormula:(NSString *)formula {
    return [NSString convertCommaFormula:formula cursorlocation:0][@"text"];
}

+ (NSDictionary *) convertCommaFormula:(NSString *)formula cursorlocation:(NSInteger)location {
    
    NSString *beforeCursorText = [formula substringWithRange:NSMakeRange(0, location)];
    NSString *removedCommaBeforeCursorText = [beforeCursorText stringByReplacingOccurrencesOfString:@"," withString:@""];
    NSString *removedText = [formula stringByReplacingOccurrencesOfString:@"," withString:@""];
    
    NSMutableString *mBeforeText = [NSMutableString stringWithString:removedCommaBeforeCursorText];
    NSMutableString *mOriginText = [NSMutableString stringWithString:removedText];
    // 12|74
    
    if (![LayoutInfo shared].isNotComma) {
        NSInteger numbercount = 0;
        for (NSInteger i=mOriginText.length-1; i>=0; i--) {
            char c = [mOriginText characterAtIndex:i];
            if ('0'<=c && c<='9') {
                numbercount++;
            } else if (c=='.') {
                numbercount = 0;
                // 다시 되돌아가면서 콤마 지우기
                for (NSInteger j=i; j<mOriginText.length; j++) {
                    char cc = [mOriginText characterAtIndex:j];
                    if (cc==',') {
                        [mOriginText replaceCharactersInRange:NSMakeRange(j, 1) withString:@""];
                        if (j<mBeforeText.length) {
                            [mBeforeText replaceCharactersInRange:NSMakeRange(j, 1) withString:@""];
                        }
                        j--;
                    } else if ('0'<=cc && cc<='9') {
                        continue;
                    } else {
                        if (i==j) continue;
                        break;
                    }
                }
            } else {
                numbercount = 0;
            }
            
            
            if (numbercount==COMMA_INDEX+1) {
                numbercount-=COMMA_INDEX;
                // i+1번째에 ,추가
                
                [mOriginText replaceCharactersInRange:NSMakeRange(i+1, 0) withString:@","];
                if (i+1<mBeforeText.length) {
                    [mBeforeText replaceCharactersInRange:NSMakeRange(i+1, 0) withString:@","];
                }
            }
        }
    }
    
    return @{@"text":mOriginText, @"cursorindex":@(mBeforeText.length)};
}


@end




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


먼저 이클립스에 cocos2d-x 환경을 세팅한다. 그리고 cocos2d-x 프로젝트를 하나 만든 다음, 그것을 안드로이드 스튜디오에서 열어볼 것이다. 

아래와 같이 13기가의 용량을 비워두고 환경 세팅을 시작했다. cocos2d-x프로젝트를 만들어 기기에 돌려보기까지 13기가를 꼬박 다 쓰고도 5기가를 추가로 다시 마련했다.

그리하여 환경세팅은 겨우겨우 끝냈지만 아직 빌드라던지 새 프로젝트를 만든다던지 하기 전이었다.

400메가가 남은 상태에서 도저히 진행이 불가능하여서 iMoive와 가비지벤드, iPhoto를 지우고 다시 진행했다. 

내가 새벽에 내내 작업을 해서 정신이 혼미했지만.. 안드로이드 스튜디오가 깔려 있는 상태에서 나머지 환경세팅을 하는데 약 20기가 정도가 들었다. 빌어먹을 NDK..
그리고 여분의 15기가를 준비하여 프로젝트를 생성, 빌드하는데 무리없도록 만들었고 가슴이 아픈 메모리 정리였다ㅠ

결론적으로 나는 총 35기가 정도의 용량으로 환경세팅을 완료했다. (확실하게 측정한 값은 아닙니다. 참고만 해주세요...)

아래는 간단하게나마 환경세팅을 하는 순서를 나열하겠다.
혹시 이미 설치된게 있다면 알아서 넘어가면 된다.

글쓴이가 기본 환경 경로로 잡은 곳은 /Users/사용자이름/Document 이다.
여기에 eclipse와 Cocos2d-x, ant, ndk를 다운 받았고, android-sdk는 eclipse폴더 안에 넣어두었다.


맥에서 Cocos2d-x 환경 세팅하기

1. 맥에 이클립스 설치 (http://toplogic.tistory.com/40)


2. 맥에 이클립스에 안드로이드(android-sdk) 설치 (http://toplogic.tistory.com/41)
$ sudo find / -name "*ndk*"
"*ndk*"의 뜻은 ndk를 포함한 모든것을 찾아달라는 뜻이다.


3. 대망의 NDK 다운 (https://developer.android.com/ndk/downloads/index.html#download)

- 글쓴이는 r10e 버전을 다운받았다.
- 다운 받고 나면 아래와 같이 압축을 풀어준다. (압축 푸는데 시간 좀 걸린다. 용량도 엄청 잡아먹는다.)
ndk$ chmod a+x android-ndk-r10c-darwin-x86_64.bin
ndk$ ./android-ndk-r10c-darwin-x86_64.bin


4. ANT를 다운 (http://ant.apache.org/bindownload.cgi)


5. ./bash_profile을 설정한다.
- $ vi /Users/사용자이름/.bash_profile 을 쳐서 ./bash_profile 파일에 들어간다.
- vi 에디터 사용법을 모른다면 대강 익히고 돌아오자..

- 위 스크린샷과 같이 경로를 지정해주면 되는데, 앞에서 다운 받았던 것들의 경로를 넣어주면 된다. (tip : 폴더를 드래그해서 터미널에 드롭하면 절대 경로를 알아낼 수 있다!)
- android-sdk, ndk, ant의 경로를 오타 없이 잘 넣자.


6. 이제 cocos2d-x를 다운받자! (http://www.cocos2d-x.org/download/version#Cocos2d-x)
- 글쓴이의 경우 v3.0을 다운받았다. (http://cdn.cocos2d-x.org/cocos2d-x-3.0.zip)


7. 다운받은 Cocos2d-x의 압축을 풀고, 터미널로 압축을 풀었던 폴더에 들어간다.


8. .bash_profile파일에 지정해놨던 경로들을 Cocos2d-x에 적용시킨다. 아래 명령어를 치면 된다.
$ ./setup.py
- 만약 뭐 경로가 하나라도 없다면 없는 경로가 있다고 알려줄 것이다.
- 경로가 잘 세팅 되있다면 아래 명령어를 치면 된다고 뜰 것이다. 아래 명령어를 치면 이제 경로 적용이 완료된 것이다.

$ source ~/.bash_profile


여기까지 잘 따라왔다면 mac에서 cocos2d-x 환경 세팅이 완료된 것이다.
아흑.. 너무 힘들었다.. 하지만 아직 프로젝트를 생성해보기 전까지 방심해서는 안된다. 젠장 첩첩산중이지만 슬슬 끝이 보인다. 


맥에서 Cocos2d-x 프로젝트 생성하기

1. 프로젝트를 생성하고 싶은 경로에 들어가서 아래 명령을 입력한다.

$ cocos new MyGameTest -p com.your_company.mygametest -l cpp -d ./mygametest
- 프로젝트 이름은 MyGameTest이고 페키지 이름은 com.your_company.mygametest이다 그리고 mygametest폴더를 만들어서 그 안에 프로젝트를 생성한다.


2. finder에서 프로젝트 폴더 안에 들어가보면 아래와 같이 나온다. 프로젝트 생성이 된거다.


아이폰 프로젝트를 Xcode로 열고 실행시켜보기

0. Xcode가 설치되있어야합니다.

1. 위 경로에서 proj.ios_mac 폴더에 들어가서 Xcode 프로젝트를 실행시킨다.

2. Xcode가 켜지면 빌드&런을 해서 시뮬레이터(혹은 단말기)에 앱이 올라가는지 보면 된다.
- 빌드하는데 시간이 생각보다 많이 걸린다.. ㅠ

이렇게 실행이 되면 성공이다.


안드로이드 프로젝트를 Android Studio에서 실행시켜보기

0. 안드로이드 스튜디오를 설치해놓아야한다.

1. 유튜브 링크에 들어가서 그대로 따라한다. 이미 프로젝트 생성까지는 했으므로 안드로이드 스튜디오에 프로젝트를 import하는 부분부터 하면 된다. (https://youtu.be/VLeGy1foMQA?t=7m51s)

2.  쭉 따라서 하면 된다. 아래와 같이 된다면 성공이다.





추가로) 이 포스팅에서는 안드로이드 빌드를 터미널에서 해보진 않았지만, 터미널에서 안드로이드 빌드를 해보는 과정에서 아래와 같은 에러가 뜨는 경우가 있다. 


$ cocos run -p android
...
...
building apk Android platform not specified, searching a default one...
Can't find right android-platform for project :"/Users/nownabe/projects/HelloCocos/proj.android".
The android-platform should be equal/larger than 10


이 문제는 이 링크(http://qiita.com/nownabe/items/496285423c74b47dcd42)를 통해서 해결할 수 있다. 비록 일본어로 되어있지만 크롬으로 켜서 번역기 돌리고 보면 나름 이해가 된다. 중간부분에 해결 방법을 설명해놓았다. 안드로이드의 버전 문제인듯 하다.







WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
이번 포스팅에서는 iOS에서 Objective-C로 푸시를 구현하는 방법에 대해 설명하겠다. 그렇지만 단순히 푸시를 받는것이 다가 아닌 푸시를 받았을 때 다음 이벤트 처리도 앱에서 어떻게 처리할지에대한 고민도 함께할 것이다. 이 부분은 애플에서 제공하는 방법이 생각보다 복잡하게 되있는것 같으므로 포스팅에 남기기로 하였다.

먼저 푸시를 받는 방법에 대해 설명하..  검색해보니 바로 원하는 링크가 안나와서 푸시 받는법부터 설명해보겠다.



1. 푸시 아이디를 얻어내는법

푸시 아이디를 얻고자 하는 시점에서 다음을 호출하면 된다. 
// 현재 푸시가 On인지 Off인지 알아내는 함수
BOOL pushEnable = NO;
if ([[UIApplication sharedApplication] respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
    pushEnable = [[UIApplication sharedApplication] isRegisteredForRemoteNotifications];
} else {
    UIRemoteNotificationType types = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];
    pushEnable = types & UIRemoteNotificationTypeAlert;
}

// 푸시 아이디를 달라고 폰에다가 요청하는 함수
UIApplication *application = [UIApplication sharedApplication];
if ([application respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
    NSLog(@"upper ios8");
    // iOS 8 Notifications
    [application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
    [application registerForRemoteNotifications];
} else {
    NSLog(@"down ios8");
    // iOS < 8 Notifications
    [application registerForRemoteNotificationTypes:
     (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound)];
}


// AppDelegate.m 파일에서 아래 함수를 추가한다. 아래 함수는 푸시 아이디를 받아내는 함수이고, 푸시아이디는 아래 함수를 통해서만 받을 수 있다.
// AppDelegate.m
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
     NSString* newToken = [[[NSString stringWithFormat:@"%@",deviceToken]
                           stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]] stringByReplacingOccurrencesOfString:@" " withString:@""];
     NSLog(@"DeviceToken : %@", newToken );
}


위 코드를 보면 이것저것 복잡해보인다. 여기서 중요한 라인만 설명을 하자면

UIApplication *application = [UIApplication sharedApplication];
[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]];
[application registerForRemoteNotifications];
위 세 라인이다.
1. 앱 싱글톤 델리게이트를 받아와서 2. 푸시받을 타입을 정한다. 그리고 3. 실제로 푸시 아이디를 달라고 요청한다.
registerForRemoteNotifications가 호출되는 순간 appdelegate.m에 
첫째, 앱을 설치하고 처음으로 registerForRemoteNotifications를 호출하는 상황이면 사용자에게 푸시를 허용할지 말지에 대한 경고창이 뜬다. 여기서 허용을 하면 AppDelegate.m에 구현된application:didRegisterForRemoteNotificationsWithDeviceToken: 함수가 호출되면서 deviceToken파라미터에 푸시아이디가 담기게 된다. 반대로 사용자가 푸시 허용을 하지 않는다면 application:didRegisterForRemoteNotificationsWithDeviceToken: 함수는 호출되지 않는다. 그게 다다. 사용자가 푸시알림 허용 안했다고해서 뭐 어떻게 더 바로 할 수 있는게 없다. 푸시알림 무한 유도를 막기위함의 애플의 생각이 아닐까 싶은데, 이 부분때문에 고민을 좀 많이했다. 그래서 준비한게 BOOL pushEnable;이고 푸시가 현재 허용되있는지 아닌지 대강 판단해준다. 푸시가 꼭 필요한 어플일 경우는 pushEnable를 받아내서 확인할 수 있다. (하지만 이것도 사용자가 한번도 푸시 설정을 안했는지, 실제 푸시 거부를 한건지 알 방법은 없다. 단지 현재 푸시 설정 On/Off 여부만 알 수 있다.)
둘째, 앱을 설치하고 푸시 허용을 해놓은 상태이며(최초 registerForRemoteNotifications를 호출 해본 상태), registerForRemoteNotifications를 호출했다면 AppDelegate.m에 application:didRegisterForRemoteNotificationsWithDeviceToken:함수가 실행되면서 푸시아이디를 받아올 수 있다. 푸시 아이디를 서버에 등록할 때 사실상 푸시 아이디는 바뀔 수가 있다. 그렇기에 나같은 경우는 푸시아이디와 디바이스아이디를 함께 서버에 보내서 디바이스 아이디 기준으로 푸시 아이디를 저장한다. 아무튼 두번째의 경우는 무조건 푸시아이디를 받아올 수 있는 상황이다.

세번째, 앱을 설치하고 푸시 허용을 해제해놓은 상태이며(마찬가지로 최초 registerForRemoteNotifications를 호출 해본 상태), registerForRemoteNotifications를 호출했다면 아무일도 일어나지 않는다. 그렇기때문에 이 경우는 미리 pushEnable를 받아내서 푸시가 가능한지 아닌지를 판단해놓을 필요가 있다. pushEnableNO이면 푸시 허용 Off인 상태인거다. 그러면 푸시 설정을 해라고 경고를 띄우면 된다. 


2. 서버에서 푸시를 쏘았을때 폰에서 받은 위 호출되는 함수
서버에서 푸시를 쏘자마자 알아내는 방법은 하나밖에 없다. -> 앱이 실행되는 중일때이다.
앱이 만약 꺼저있거나, 백그라운드에서 돌고있으면 푸시가 왔는지 안왔는지 앱에서는 모른다. 앱이 다시 포그라운드로 왔을때 푸시가 왔으면 ‘푸시받는함수'가 호출된다. 하지만 웃긴것이, 앱이 백그라운드에도 없고 메모리에 올라가있지 않을때 푸시가 왔을때, 바탕화면에서 푸시를 받아서 푸시를 눌러 앱에 들어가면 ‘푸시받는함수’가 호출되지 않는다. 이 예외적인 부분을 처리하는 방법에 대해 설명하겠다.
 
// AppDelegate.m
// 앱이 런칭되서 메모리에 올라갈때 실행되는 함수이다.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    UILocalNotification *notificationUserInfo = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    if (notificationUserInfo) {
//        NSLog(@"app recieved notification from remote%@",notification);
        NSMutableDictionary *mNotificationUserInfo = [NSMutableDictionary dictionaryWithDictionary:(id)notificationUserInfo];
        mNotificationUserInfo[@"appLaunch"] = @"yes";
        [self application:application didReceiveRemoteNotification:mNotificationUserInfo];
    }else{
//        NSLog(@"app did not recieve notification");
    }
    return YES;
}

여기서
// AppDelegate.m
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {

    [PushController shared].userInfo = userInfo;

    if (userInfo[@"appLaunch"]) {
        [PushController shared].waitingViewDidLoad = YES;
    } else {
        if (application.applicationState==UIApplicationStateActive) {
            [[PushController shared] pushInActiveStatus];
        } else {
            // if (application.applicationState==UIApplicationStateInactive) {
            [[PushController shared] presentPushPostTableViewController];
        }
    }
}
위의 PushController는 일단 신경쓰지 말고 푸시를 받았을 때 라이프 사이클만 신경쓰자. 
저기서 특이한 점은 
if (userInfo[@"appLaunch”])
를 이용하여 첫째 상황인지 먼저 구분하고
if (application.applicationState==UIApplicationStateActive) ;
를 이용하여 둘째상황과 셋째상황을 구분했다는 점이다.

첫째 상황에서는 아직 ViewController가 생성이 되기 전의 라이프사이클이다. 그렇기때문에 ViewControllerviewDidLoad를 호출하기 전까지 푸시정보를 잠시 가지고 있어야한다. 
둘째 상황에서는 application.applicationState==UIApplicationStateInactive에 해당하는 상태인데, 현재 앱이 백그라운드에서 포그라운드로 가고 있다는 뜻으로 사용자가 앱을 켜놓고 바탕화면에서 푸시를 눌렀을 때를 말한다. 이때는 바로 푸시 행위를 진행하면 된다.
셋째상황은 application.applicationState==UIApplicationStateActive의 상황으로써 앱이 이미 포그라운드에서 돌고 있었다는 뜻이고 이때는 자동으로 푸시 기능을 실행시켜버리는게 아니라 상단에 팝업창이 떠서 푸시가 온것처럼 만들어주면 사용자가 인지하기 쉬울 것이다. (안드로이드는 기본으로 제공되고, 아이폰은 카톡에서 볼 수 있는 기능이다) 상단에서 팝업이 잠시 내려왔을때 팝업을 누르면 푸시 기능을 수행하게 만들었는데, 이 기능은 PushController에 있다. PushController는 UI작업을 좀 했기 때문에 라인이 좀 되서 첨부해두겠다.

아무튼 위와같이 처리하면 푸시를 받는 일을 놓히지 않고 처리할 수 있다. 
 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
1. 구글 드라이브 web host 폴더 만들기
1) 구글 드라이브 가입하기
2) 구글 드라이브 컴퓨터에 설치하기
3) 배포용 폴더 만들기 (본인의 경우 Google 드라이브>2015 학교>수업>안드로이드실험>프로젝트>ios_APP의 경로로 설정했다)
4) 배포용 폴더를 웹호스팅 공개로 설정


빨간색 네모박스에 있는 키값을 꼭 복사해놓는다. (내 폴더 키는 소중하니까 모자이크처리)
맨 마지막에 URL 만들때 필요하다!

다음으로 고급을 누른다.

변경을 누른다.

'웹에 공개’를 누르고 저장을 누르면 웹 호스팅 폴더가 완성됬다!

5) Finder에서 확인해보면 폴더 모양이 공유된거처럼 생겼다.



2. Adhoc용으로 ipa파일 만들기
1) AdHoc용 Provisioning Profiles을 만들고 더운받아서 실행한다.
-> 아마 Xcode가 실행될 것이다.

2) Product>Archive하기 전에 세팅을 한다.
-> Adhoc으로 Archive(필드와 비슷한 것)해야하기 때문에 세팅을 한다.
- Project>General에서 Team설정 : None에서 자신의 계정으로 설정
- Project>Build Setting에서 Code Signing>Code Signing identity를 iPhone Distribution:XXXXX로 설정
- Project>Build Setting에서 Code Signing>Provisioning Profile을 1번에서 만든 AdHoc Provisioning Profile로 설정

3) Product>Archive를 실행


주의사항 : 시뮬레이터가 선택되있으면 Archive가 비활성되있다. 위 스샷처럼 iOS Device를 선택해주자.

4) 빌드가 끝나면 새로운 창이 뜨면서 어떤식으로 배포할 것인지 정하는 프로세스가 나올것이다. export를 하여 Adhoc을 고르고 구글 드라이브와 연동되있는 폴더에 ipa파일을 생성하면 된다.

3. BetaBuilder를 이용하여 URL만들기

1) choose IPA…버튼을 눌러서 .ipa파일을 가져온다.

2) 하단에 Full Web Deloyment Path를 입력한다. 
-> 구글 드라이브의 경우 https://googledrive.com/host/XXXXXXXXXXXXX
XXXXXXXXXXX부분에는 1.4번 구글드라이브 폴더에서 가져온 ipa파일이 들어있는 해당 디렉토리의 키값이다.


3) Generate Depolyment Files… 버튼을 눌러서 .ipa파일이 들어있는 폴더에 Generate 시킨다.


4. 링크 배포하기
UDID를 등록한 핸드폰(혹은 패드)에서 위 링크를 타고 들어가면 html이 열릴 것이다.
'Tap Here to install’을 누르면 다운이 시작될 것이다.

아래 스샷은 맥에서 연 사진이지만, 단말기의 브라우저에서 열어야한다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


-(void) setMaskTo:(UIView*)view byRoundingCorners:(UIRectCorner)corners cornerRadius:(CGFloat)cornerRadius{
    UIBezierPath* rounded = [UIBezierPath bezierPathWithRoundedRect:view.bounds
                                                  byRoundingCorners:corners
                                                        cornerRadii:CGSizeMake(cornerRadius, cornerRadius)];
    
    CAShapeLayer* shape = [[CAShapeLayer alloc] init];
    [shape setPath:rounded.CGPath];
    view.layer.mask = shape;
}


view.frame = CGRectMake...하고나서
[self setMaskTo:view byRoundingCorners:...];
을 호출해주어야지 정상적으로 라운딩된 뷰를 볼 수 있다. 


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

1. 일단 뷰를 생성해 둔다.

// view 생성
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor colorWithWhite:0.f alpha:1.f];
view.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:view];

x, y : 100, 100
w, h : 100, 100
배경색 : 검정 


2. 테두리

// 테두리
view.layer.borderColor = [UIColor colorWithRed:1.f green:0.f blue:0.f alpha:1.f].CGColor;
view.layer.borderWidth = 1.f;

빨간색의 너비가 1인 테두리를 만든다


3. 둥근 모서리

// 둥근 모서리
view.layer.cornerRadius = 10.f;

 

아래와 같이 둥근 모서리를 이용한 원 형태의 뷰도 만들 수 있다.

UIImageView *iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"profile.jpg"]];
iv.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:iv];


iv.clipsToBounds = YES;
iv.layer.cornerRadius = iv.frame.size.width/2.f;



4. 스케일 변경 및 애니메이션

// 2배로 크게
view.transform = CGAffineTransformMakeScale(2.0, 2.0);

각 너비와 높이의 스케일 값을 넣는다
애니메이션 효과를 원한다면 아래와 같이 하면 된다.

// 애니메이션
[UIView animateWithDuration:3.f animations:^{
    view.transform = CGAffineTransformMakeScale(2.0, 2.0);
}];



5. 각도 변경 및 애니메이션

// 180도 회전
view.transform = CGAffineTransformMakeRotation(M_PI);

M_PI는 미리 정의된 3.141592... 파이값이다. 저 값만 바꿔주면 되며 
애니메이션 효과를 원한다면 아래와 같이 하면 된다.

// 애니메이션
[UIView animateWithDuration:3.f animations:^{
    view.transform = CGAffineTransformMakeRotation(M_PI);
}];





CALayer

The CALayer class manages image-based content and allows you to perform animations on that content. Layers are often used to provide the backing store for views but can also be used without a view to display content. A layer’s main job is to manage the visual content that you provide but the layer itself has visual attributes that can be set, such as a background color, border, and shadow. In addition to managing visual content, the layer also maintains information about the geometry of its content (such as its position, size, and transform) that is used to present that content onscreen. Modifying the properties of the layer is how you initiate animations on the layer’s content or geometry. A layer object encapsulates the duration and pacing of a layer and its animations by adopting the CAMediaTiming protocol, which defines the layer’s timing information.

CALayer 클래스는 컨텐츠를 기반으로한 이미지를 관리하거나 컨텐츠의 애니메이션 기능을 수행하는 역활을 한다. 레이어는 보통 뷰들의 역할을 보완하는데 사용되기도 하지만 뷰 없이도 콘텐츠를 나타내는데 사용될 수 있다. 레이어의 주된 기능은 당신이 제공한 콘텐츠를 보여주는 역할을 관리하는 것이다. 그러나 레이어 그 자신이 배경색이나 테두리, 그림자와 같은 시각적인 속성을 가지고 있다. 추가적으로 시각적으로 콘텐츠를 관리하기 위해, 레이어는 화면에 보여지고 있는 기하학적인 정보들(좌표, 크기, transform등등)을 가지고있다. 레이어의 속성을 변화시키는 것은 콘텐츠의 레이어나 기하학적으로 애니메이션을 시작할 수 잇는 방법이다. 레이어 오브젝트는 레이어의 지속시간이나 페이스를 담고있고 이것은 CAMediaTiming를 적용시킴으로써 레이어에 정의된 시간적인 정보에 따라 애니메이션 시킬 수 있다.




If the layer object was created by a view, the view typically assigns itself as the layer’s delegate automatically, and you should not change that relationship. For layers you create yourself, you can assign a delegate object and use that object to provide the contents of the layer dynamically and perform other tasks. A layer may also have a layout manager object (assigned to the layoutManager property) to manage the layout of subviews separately. 

만약 뷰에 의해 레이어 오브젝트가 생성되었다면, 뷰는 일반적으로 레이어 델리게이트에 자동으로 지정되있을 것이고 당신은 이 지정을 바꿀 수 없다. 당신이 직접 만든 레이어의 한해서, delegate 오브젝트에 어싸인 할 수 있고 그 델리게이트 오브젝트는 콘텐츠 레이어의 동적인 행동이나 다른 일들을 제공하는 것들을 사용할 수 있다. 레이어는 또한 차별적으로 subview들의 레이아웃을 관리하기 위한 (
layoutManager 속성에 어싸인된) 레이아웃 메니저 오브젝을 가진다.







WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

1. 블럭 코딩
나는 애니메이션 효과를 쓸 때는 블럭코딩을 주로 사용하는 편인데, 보기도 편하고 사용하기 좋다.
사용법은 아래와 같이 사용하면 된다.

[UIView animateWithDuration:3.f animations:^{
    // 애니메이션 ..
}];


2. CGRect 변경 (좌표 및 사이즈) 애니메이션

view.frame = CGRectMake(0, 0, 100, 100);

[UIView animateWithDuration:3.f animations:^{
    view.frame = CGRectMake(100, 100, 50, 50);
}];



3. 투명도 변경 애니메이션

view.frame = CGRectMake(100, 100, 100, 100);

[UIView animateWithDuration:3.f animations:^{
    view.alpha = 0;
}];







WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,



구글 애널리틱스가 뭔지부터 설명하겠다. 이놈은 구글에서 만든 "서비스 분석기”로 웹이나 앱에서 사용자가 서비스(웹이나 앱등을)를 어떻게 실행했는지 얼마나 실행했는지를 분석해서 통계를 내주고 도식화해주는 그런 놈이다. 그러니까 쉽게 예를 들어보면 '하루에 몇명이 접속했는지’, ‘실시간으로 몇명이 접속해있는지’ 이런 정보들을 제공한다. 또한 이런 방대한 기능에 비해서 앱에 적용해 사용하기가 쉽다! (아이고 감사합니다) 한번 구글 애널리틱스를 사용해본 개발자라면 다음부턴 항상 사용하게 될 것을 장담한다.

좀 더 자세한 설명을 보고싶다면 구글애널리틱스 자세히 알아보기 여기 들어가면 구글에서 자세히 설명해놓았다.

이번 포스팅에서는 iOS앱에서 간단히 적용시키는 것에 초점을 맞춰서 글을 써내려갈 생각이다.



구글 애널리틱스 사이트에 내 앱 등록하기

1. 구글 아이디 만든다 (웬만하면 있지요들? 근데 개발자용 하나 따로 만들어 놓으면 편합니다)

2. 구글 애널리틱스 홈페이지가서 가서 구글 애널리틱스 가입을 합니다. (구글 계정이 필요) 오른쪽 상단에 계정만들기 눌러서 진행하면 됨

3. 서비스 등록
- 처음 가입했다면 바로 아래와 같은 창이 뜰것이고

- 이미 가입한 상태라면 

상단에 “관리”라는 탭이 있다. 그걸 눌러서 

“새 속성 만들기”를 누르면 새로운 서비스를 등록할 수 있다.

4. 이것저것 작성하고나서 "추적 ID가져오기” 버튼을 누르면 추적 아이디가 생긴다.
예) DB-271282139-1 이런 모양으로 생겼다. 기억해두고 나중에 코드에 삽입해야한다.


구글 애널리틱스 SDK를 앱에 심기
1. 구글 애널리틱스 SDK를 다운받는다. 다운로드 링크
zip파일로 되있고 3.10버전(14년11월15일 기준)이라고 되있다.


2. 프로젝트에 추가해야할 .h파일들과 .a파일이다.
libGoogleAnalyticsServices.a
그 외 .h파일들을 집어 넣는다.


이렇게 GoogleAnalytics폴더 하나 만들어서 필요한 파일들 넣어주면 된다.

3. 이제 필요한 프레임 워크를 넣을거다

아이고 친절하다

  • CoreData.framework
  • libAdIdAccess.a
  • AdSupport.framework
  • SystemConfiguration.framework
  • libz.dylib
  • libsqlite3.dylib


환경 세팅은 끝났다.
코드 작성하러 가자

4. 코드 작성

AppDelegate.m에 임포트 시킨다.

#import "GAI.h"
#import "GAIDictionaryBuilder.h"
#import "GAIFields.h"

AppDelegate의 application:didFinishLaunchWithOptions:메서드에 다음 코드를 추가한다.
(그냥 초기화, 초기세팅 정도의 기능을 하지만 사실 없어도 동작하더라..)

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Optional: automatically send uncaught exceptions to Google Analytics.
    [GAI sharedInstance].trackUncaughtExceptions = YES;
    // Optional: set Google Analytics dispatch interval to e.g. 20 seconds.
    [GAI sharedInstance].dispatchInterval = 5;
    // Optional: set Logger to VERBOSE for debug information.
    [[[GAI sharedInstance] logger] setLogLevel:kGAILogLevelVerbose];
    // Initialize tracker.

    return YES;
}


그리고 이제 실제로 서버에 Hit을 날려줄 코드를 작성한다.
나같은 경우 ViewController.m에 viewDidLoad에다가 넣었다.

#define GOOGLE_ANALYTICS_KEY @"DB-271282139-1"

아까 내가 기억해놓으라고 했던 추적ID를 이제 쓸 차례가 왔다.

- (void) viewDidLoad {
    [super viewDidLoad];

    id<GAITracker> tracker = [[GAI sharedInstance] trackerWithTrackingId:GOOGLE_ANALYTICS_KEY];
    [tracker set:kGAIScreenName value:@"테스트화면1"];
    [tracker send:[[GAIDictionaryBuilder createAppView] build]];
}


마무리
이렇게 하면 이제 앱이 켜지고 저 화면이 켜질때마다 알아서 구글 애널리틱스에 hit을 시킨다. 위치정보나 시간정도 이런거 주서다가 보내주는 모양이다. 나중에 구글 애널리틱스 홈페이지 들어가서 통계자료를 보면 되고,
실제 앱이 잘 연동되었는지 확인을 하려면 앱을 실행시켜놓고 구글애널리틱스 홈페이지 들어가서 전체 모바일 데이터 (All Mobile Data)를 눌러서 실시간 왼쪽탭의 메뉴를 눌러보면 1명이 활성화 된것을 확인 할 수있다. 오오 신기하다.

왜 구글 애널리틱스인가?
일단 수많은 이유가 있겠지만
첫째 공짜이고 
둘째 공짜이고 
셋째 공짜이고 
넷째 연동하기 너무 쉽고 
다섯째 통계를 다양한 관점에서 잘 분석해 놓았다.(앱이 강제 종료 되는것도 알아서 통계가 나옴)

그리고 최종적인 목적은 내 서비스를 다음에 어떤식으로 개발해야할지 업데이트 해야할지 방향이 서기 때문이다.




아무튼 글을 마치겠습니다. 오류 지적이나 여러가지 의견은 댓글로 달아주시면 감사하겠습니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

준비할것

카카오톡 API 홈페이지 들어가서 준비를 한다.
- 나머지 프레임 워크도 추가한다.

#import <MessageUI/MessageUI.h>
#import <KakaoOpenSDK/KakaoOpenSDK.h>
#import <Social/Social.h>
#import <Accounts/Accounts.h>


MFMessageComposeViewControllerDelegate 델리게이트를 등록한다. 메시지 전송 팝업을 컨트롤 할때 사용한다.

@interface ViewController () <MFMessageComposeViewControllerDelegate> {

}
@end


아래 코드는 실제 동작하는 코드

- (void) shareWithIndex:(NSInteger)buttonIndex text:(NSString *)text image:(UIImage *)image imageURLString:(NSString *)imageurl/*카톡 이미지 공유에서 쓰임*/ url:(NSURL *)url { if (buttonIndex==0) { // 문자 메세지 [self shareMessageWithText:text image:image url:url]; } else if (buttonIndex==1) { // 카카오톡 if ([KOAppCall canOpenKakaoTalkAppLink]) { // 카카오톡 공유 [self kakaoWithText:text image:image imageURLString:imageurl url:url]; } else { // 카카오톡 설치 [self openInstallKakaoAlert]; } } else if (buttonIndex==2) { // 페이스북 [self shareWithServiceType:SLServiceTypeFacebook Text:text image:image url:url]; } else if (buttonIndex==3) { // 트위터 [self shareWithServiceType:SLServiceTypeTwitter Text:text image:image url:url]; } } #pragma mark - 메시지 - (void) shareMessageWithText:(NSString *)text image:(UIImage *)image url:(NSURL *)url { if(![MFMessageComposeViewController canSendText]) { UIAlertView *warningAlert = [[UIAlertView alloc] initWithTitle:@"메시지 보내기 기능을 지원하지 않습니다." message:@" " delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [warningAlert show]; return; } else { MFMessageComposeViewController *controller = [[MFMessageComposeViewController alloc] init]; if([MFMessageComposeViewController canSendText]) { controller.body = [NSString stringWithFormat:@"%@\n\n%@", text, url]; // controller.recipients = recipients; controller.messageComposeDelegate = self; NSData *data = UIImageJPEGRepresentation(image, 0); [controller addAttachmentData:data typeIdentifier:@"image/jpg" filename:@"thumbnail"]; [self presentViewController:controller animated:YES completion:nil]; } } } #pragma mark - 메시지 전송 delegate - (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result { [controller dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - 카카오톡 - (void) kakaoWithText:(NSString *)text image:(UIImage *)image imageURLString:(NSString *)imageurl url:(NSURL *)url { // 카카오톡 KakaoTalkLinkAction *androidAppAction = [KakaoTalkLinkAction createAppAction:KakaoTalkLinkActionOSPlatformAndroid devicetype:KakaoTalkLinkActionDeviceTypePhone marketparam:nil execparam:@{@"kakaoFromData":[NSString stringWithFormat:@"{seq:\"%@\", type:\"%@\"}", self.dataInfo[@"contentsSeq"], self.dataInfo[@"contentsType"]]}]; KakaoTalkLinkAction *iphoneAppAction = [KakaoTalkLinkAction createAppAction:KakaoTalkLinkActionOSPlatformIOS devicetype:KakaoTalkLinkActionDeviceTypePhone marketparam:nil execparam:@{@"kakaoFromData":[NSString stringWithFormat:@"{seq:\"%@\", type:\"%@\"}", self.dataInfo[@"contentsSeq"], self.dataInfo[@"contentsType"]]}]; NSString *buttonTitle = @"앱으로 이동"; NSMutableArray *linkArray = [NSMutableArray array]; KakaoTalkLinkObject *button = [KakaoTalkLinkObject createAppButton:buttonTitle actions:@[androidAppAction, iphoneAppAction]]; [linkArray addObject:button]; /*[NSString stringWithFormat:@"%@ (%@)",[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"], LOC(@"msg_invite_kakao", @"경영전문대학원 MBA 모바일 주소록 앱")]*/ if (text) { KakaoTalkLinkObject *label; label = [KakaoTalkLinkObject createLabel:text]; [linkArray addObject:text]; } if (imageurl && image) { KakaoTalkLinkObject *kimage = [KakaoTalkLinkObject createImage:imageurl/*self.dataInfo[@"thumbnail1"]*/ width:image.size.width height:image.size.height]; [linkArray addObject:kimage]; } [KOAppCall openKakaoTalkAppLink:linkArray]; } - (void) openInstallKakaoAlert { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"카카오톡이 설치되어 있지 않습니다." message:@"카카오톡을 설치하겠습니까?"// @"Do you want to install the KakaoTalk?" delegate:self cancelButtonTitle:@"취소" otherButtonTitles:@"확인", nil]; alert.tag = 141; [alert show]; } #pragma mark - Alert View Delegate - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (alertView.tag==141) { if (buttonIndex==[alertView cancelButtonIndex]) { // cancel } else { // 카카오톡 링크로 이동 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://itunes.apple.com/kr/app/id362057947"]]; } } } #pragma mark - 페이스북 트위터 - (void) shareWithServiceType:(NSString *)serviceType Text:(NSString *)text image:(UIImage *)image url:(NSURL *)url { if ([SLComposeViewController isAvailableForServiceType:serviceType]) { SLComposeViewController *mySLComposerSheet = [SLComposeViewController composeViewControllerForServiceType:serviceType]; if (text) [mySLComposerSheet setInitialText:text]; if (image) [mySLComposerSheet addImage:image]; if (url) [mySLComposerSheet addURL:url]; [mySLComposerSheet setCompletionHandler:^(SLComposeViewControllerResult result) { switch (result) { case SLComposeViewControllerResultCancelled: NSLog(@"Post Canceled"); break; case SLComposeViewControllerResultDone: NSLog(@"Post Sucessful"); break; default: break; } }]; [self presentViewController:mySLComposerSheet animated:YES completion:nil]; } else { [[[UIAlertView alloc] initWithTitle:@"실패" message:@" " delegate:nil cancelButtonTitle:@"확인" otherButtonTitles:nil] show]; } }


페이스북, 트윗 등등 다른 기본 소셜 공유기능을 사용하려면

SOCIAL_EXTERN NSString *const SLServiceTypeTwitter NS_AVAILABLE(10_8, 6_0);
SOCIAL_EXTERN NSString *const SLServiceTypeFacebook NS_AVAILABLE(10_8, 6_0);
SOCIAL_EXTERN NSString *const SLServiceTypeSinaWeibo NS_AVAILABLE(10_8, 6_0);
SOCIAL_EXTERN NSString *const SLServiceTypeTencentWeibo NS_AVAILABLE(10_9, 7_0);
SOCIAL_EXTERN NSString *const SLServiceTypeLinkedIn NS_AVAILABLE(10_9, NA);

상수 스트링을 사용하면 된다. 이 포스팅에서는 SLServiceTypeFacebook, SLServiceTypeTwitter 만 사용했다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

키보드 올라오는 Delegate받기

- (void)viewDidAppear:(BOOL)animated {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(keyboardOnScreen:) name:UIKeyboardWillShowNotification object:nil];
    [center addObserver:self selector:@selector(keyboardHideScreen:) name:UIKeyboardWillHideNotification object:nil];
}
- (void)viewDidDisappear:(BOOL)animated {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [center removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

#pragma mark - 키보드
-(void)keyboardOnScreen:(NSNotification *)notification {
    //...
}
-(void)keyboardHideScreen:(NSNotification *)notification {
    //...
}



- (void) viewDidAppear:(BOOL)animated ; 는 화면이 나타나기 바로직전에 호출되는 함수이고

- (void) viewDidDisappear:(BOOL)animated ; 는 화면이 없어지기 바로 직전에 호출되는 함수이다.

viewDidAppear에 "키보드 올라오면 말해주세요" 용도의 노티피케이션을 설정하고

viewDidDisappear에 "아까 등록한 노티피케이션을 지워주세요"라고 설정한다.




#pragma mark - 키보드
-(void)keyboardOnScreen:(NSNotification *)notification {
    NSDictionary *info  = notification.userInfo;
    NSValue      *value = info[UIKeyboardFrameEndUserInfoKey];
    
    CGRect rawFrame      = [value CGRectValue];
    CGRect keyboardFrame = [self.view convertRect:rawFrame fromView:nil]; /*키보드 프레임*/

    NSDictionary *userInfo = notification.userInfo;
    NSNumber *durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey];  /*키보드가 올라오는 동안의 시간*/
    NSTimeInterval animationDuration = durationValue.doubleValue;
    NSNumber *curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey];       /*키보드 애니메이션 옵션 효과 (ease)*/
    UIViewAnimationCurve animationCurve = curveValue.intValue;
    
    [UIView animateWithDuration:animationDuration delay:0.f options:animationCurve<<16 animations:^{
        // ...
    } completion:nil];
}

-(void)keyboardHideScreen:(NSNotification *)notification {
    NSDictionary *userInfo = notification.userInfo;
    NSNumber *durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey];
    NSTimeInterval animationDuration = durationValue.doubleValue;
    NSNumber *curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey];
    UIViewAnimationCurve animationCurve = curveValue.intValue;


    
    [UIView animateWithDuration:animationDuration delay:0.f options:animationCurve<<16 animations:^{
        // ...
    } completion:nil];
}


// ... 부분에 애니메이션 코드를 넣어주면 키보드에서와 동일한 애니메이션 ease가 연출된다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

이미지를 보여주는 앱을 만들다가 UIImageView는 GIF를 지원하지 않는걸 깨달았다. 여러가지 오픈소스를 사용해봤지만 FLAnimatedImage 이게 제일 맘에 든다. 빠르고 빠르고 빠르고 빠르다. 메모리도 적게 먹는다. 심지어 가장 최근에 만들어진듯하다. 만세다.

Github : FLAnimatedImage


사용법

FLAnimatedImage, FLAnimatedImageView파일 두개 프로젝트에 추가하고나서 아래 코드처럼 사용하면 된다.

#import "FLAnimatedImage.h"
#import "FLAnimatedImageView.h"


    FLAnimatedImage * /*__block*/ animatedImage2 = nil;
    NSURL *url2 = [NSURL URLWithString:@"http://raphaelschaad.com/static/nyan.gif"];
    NSData *data2 = [NSData dataWithContentsOfURL:url2];
    animatedImage2 = [[FLAnimatedImage alloc] initWithAnimatedGIFData:data2];
    
    FLAnimatedImageView *gifImageView = [[FLAnimatedImageView alloc] init];
    [gifImageView performSelectorOnMainThread:@selector(setAnimatedImage:) withObject:animatedImage2 waitUntilDone:NO];






WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
// NSDate -> NSStirng
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];

NSDate *now = [[NSDate alloc] init];
NSString *nowText = [dateFormatter stringFromDate:now];          // 2014-08-28 04:12:21



// NSStirng -> NSDate
NSString *dateString = @"2014-08-28";
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd"]; 

NSDate *dateFromString = [dateFormatter stringFromDate:dateString];     // NSDate로 바뀜..

WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


나는 UIKit보다 Cocos2d-iPhone을 먼저 접했다. 애니메이션에 대한 욕심이 있었기때문에.. 그치만 Cocos2d는 게임 엔진에 최적화된 라이브러리다. 유틸리티 앱을 만드는 내 입장에서는 여간 부담스럽지 않을 수 없었고.. 점점 Cocos2d를 없애나가는 방법을 찾기 시작했다.

오 그런데 신기하게도 왠만한 애니메이션은 UIKit 프레임워크에서 지원을 해줬던 것이다.

그 와중에 UIKit에서 쉽게 구현하기 힘든 기능이 EaseIn, EaseOut 기능인데, 이건 어떤 사람이 잘 만들어 놓은 라이브러리가 있다. 그걸 가져다 쓰면 된다.


Github : UIView-EasingFunctions


사용법
[UIView animateWithDuration:.6 animations:^{    
    [view setEasingFunction:ElasticEaseOutforKeyPath:@"center"];    
    view.center=CGPointMake(160,415);
}completion:^(BOOL finished){        
    [view removeEasingFunctionForKeyPath:@"center"];
}];


혹시 블럭 코딩을 잘 모르겠으면 구글에 꼭 검색해서 대충 감 잡고 사용해보시길. 난 기초가 부족하니 기초설명은 하지 않겠음.

[MyClass callBlock:^{
    // ...
}];

이렇게 생긴걸 블럭(block)이라 한다


► EaseIn, EaseOut은 점점 빠르게 혹은 점점 느리게 이런 효과를 말하는거다.

아래 다양한 Ease효과를 그래프로 그려놓은 이미지 참조




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

계산기 만들때 썼던거

int num = 300000;
NSString *numberString = [NSNumberFormatter localizedStringFromNumber:@(num) numberStyle:NSNumberFormatterDecimalStyle];
// numberString : 300,000

.. 작년에 알았더라면 귀찮게 쉼표 달아주는 함수를 따로 만들 필요도 없었을텐데.....ㅜㅜ 


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

한글 시계


가격
$0.99

첫 버전 출시
2014년 10월

다운로드 수
약 2,000건 다운 (무료+유료 2014.11 기준)

설명
한글날 출시된 “한글 시계” 앱은 텍스트를 한글만을 이용하여 만들었습니다. 어색하지 않는 UI 배치를 하려고 많이 고심했습니다. 

특징
- 앱스토어 최초 한글 시계
- 거부감이 들지 않는 UI 배치
- 편안한 애니메이션 효과
- 여러 설정 기능들


스크린샷










다운로드 링크 : https://itunes.apple.com/us/id923856886
피드백 : yapprj@gmail.com






WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

Guitar Kit+


가격
$1.99

첫 버전 출시
2014년 1월

다운로드 수
약 60,000건 다운 (무료+유료 2014.11 기준)

설명
기본에 충실한 iOS용 계산기 어플입니다. 아이폰 아이패드 둘다 지원을 하며, 기본 계산기 기능 + 여러 추가 기능을 합쳐서 만들었습니다. 

특징
- 전문적인 앱임에도 불구하고 아름다운 UI를 지원
- 약 4000개의 코드 디비
- 타이핑을 이용한 코드 검색
- 자판을 터치하여 코드 검색
- 자주 쓰는 코드 저장
- 메트로놈 기능
- 초보를 위한 28개 기본 코드 연습 기능



소개 영상


스크린샷






다운로드 링크 : https://itunes.apple.com/app/id791551793
피드백 : guitar.chord.plus@gmail.com





WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


쉬운 계산기


가격
무료

첫 버전 출시
2013년 4월

다운로드 수
약 40,000건 다운 (2014.11 기준)

유저 활성 현황
하루 약 4,000명이 앱을 사용하고 있으며 아무 홍보 없이 매일 50건이상씩 다운로드가 일어나고 있음

설명
기본에 충실한 iOS용 계산기 어플입니다. 아이폰 아이패드 둘다 지원을 하며, 기본 계산기 기능 + 여러 추가 기능을 합쳐서 만들었습니다. 

특징
- 쉬운 화면 인터페이스
- 직관적인 터치 애니메이션
- 다항 연산
- 00버튼
- %(백분율) 연산
- 딜리트(한글자씩 지우기) 기능
- 내보내기, 불러오기 기능
- 앱이 꺼져도 계산 기록 보족
- 아이폰, 아이패드 지원


스크린샷






다운로드 링크 : https://itunes.apple.com/app/id626808258
피드백 : easi.calculator@gmail.com




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

※ 번역 작업 처음 해봅니다.

※ 발번역 죄송합니다.. 틀린점 댓글로 달아주시면 수정하겠습니다.

※ 블로그에 글 올리는 것도 처음 해봅니다. 눈에 거슬리는 점이 있어도 너그럽게 이해해주시길 바라겠습니다.




원문 : Streaming Audio to Multiple Listeners via iOS' Multipeer Connectivity

 Tony DiPasquale  November 20, 2013 

Translate by canapio




음악은 아이폰 혹은 모든 애플 제품에서 굉장히 중요한 부분이다. iOS7이 출현하면서, 애플은 NSOutputStreamNSInputStream를 이용하여 데이터 스트림에 접근이 가능한 "Multipeer Connectivity"이라는 새로운 기술을 선보였다. 그러나, NSOutputStream를 이용해 재생하는 것은 쉬운일이 아니었고, 나는 CoreAudio를 이용해 사용할 수 있게 만드는 모험을 시작했다.

개요
Multipeer Connectivity는 NSOutputStream를 이용하여 연결된 요소(connected peer)에 데이터를 스트림한다. 이것은 오디오 데이터를 전송하는데 사용할 것이다. 전송이 끝나고, Multipeer Connectivity는 우리가 incoming data를 얻는데 사용할 NSInputStream 를 쓴다. 애플에서 제공한 Audio Queue Services을 사용하여, 이 데이터를 디바이스 시스템에 보낼 것이다. Audio Queue Services는 버퍼를 치우고 재생까지 할 수 있게 해준다. 이것은 오디오 데이터 열(audio data raw)을 재생시킬 수 있지만, MP3나 AAC와 같은 대부분의 오디오 파일은 크기를 줄이기 위해 인코딩작업이 되어 있다. 애플은 인코딩된 오디오 포맷을 처리하고 오디오 데이터 열을 반환해주는 Audio File Stream Services를 제공했다. 아래 그림은 데이터의 플로우이면서 계획된 솔루션이 실행되기 직전의 모습이다.



첫째로, 오디오 스트림이 시작되고 데이터를 받으면, 디코딩을 해주는 스트림 파서에 넣는다. 이 파서는 우리가 필요로하는 오디오 데이터 열(audio data raw)를 보내준다. 파서로부터 하나씩 받은 세개의 오디오 버퍼 데이터가 오디오 큐(audio queue)안에 있다. 버퍼가 다 차면 시스템으로 보내진다. 그리고 시스템에서 소리를 다 냈으면 다시 돌아와 다시 채워지고, 소리내고 비우고 채우는 과정을 더이상 플레이할 것이 없을 때까지 반복한다. 아래 GIF는 시스템 하드웨어에서 오디오 데이터가 코드에서 어떻게 흘러가는지 보여주는 애니메이션이다. 빨간 박스는 빈 버퍼, 초록 박스는 가득찬 버퍼를 의미한다.




오디오 데이터 보내기
이제 우리는 스티리밍이 백그라운드에서 어떻게 동작하는지 좀 안다. 아이튠즈 라이브러리에서 노래 한곡을 재생해보자. MPMediaPickerController을 사용하여 유저는 노래를 고를 수 있다. 우리는 피커컨트롤러의 델리게이트 메소드(mediaPicker:didPickMediaItems:)를 이용하여 MPMediaItem들을 담은 배열을 얻을 것이다. 
MPMediaItem는 곡의 타이틀, 작사, 작곡 등.. 수많은 프로퍼티를 가지고 있지만, MPMediaItemPropertyAssetURL 프로퍼티에 초점을 둘것이다. AVAssetReader 와 AVAssetReaderTrackOutput 을 사용해서 데이터 파일의 위치를 알아낸 다음 저것(MPMediaItemPropertyAssetURL)을 사용하여 AVURLAsset 를 만들어낸다. 


NSURL *url = [myMediaItem valueForProperty:MPMediaItemPropertyAssetURL];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:asset error:nil];
AVAssetReaderTrackOutput *assetOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:asset.tracks[0] outputSettings:nil];
[self.assetReader addOutput:self.assetOutput];
[self.assetReader startReading];
이제, 미디어 아이템으로부터 AVURLAsset를 뽑아냈다. 우리는 이걸 사용해서 AVAssetReader 와 AVAssetReaderTrackOutput를 만들것이다. 마지막으로 우리는 읽'어주는 놈(reader)'한테 output을 던저주고 읽게 할 것이다. startReading 이라는 메소드는 읽어주는 놈(reader)을 열고 데이터를 요청했을때를 위해 준비하는 일밖에 하지 않는다.

다음으로 NSOutputStream 을 열고 해당 델리게이트 메서드는 NSStreamEventHasSpaceAvailable 이벤트가 호출 될때까지 읽어주는 놈(reader)의 데이터를 보냅니다.


CMSampleBufferRef sampleBuffer = [assetOutput copyNextSampleBuffer];

CMBlockBufferRef blockBuffer;
AudioBufferList audioBufferList;

CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(AudioBufferList), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer);

for (NSUInteger i = 0; i < audioBufferList.mNumberBuffers; i++) {
    AudioBuffer audioBuffer = audioBufferList.mBuffers[i];  
    [audioStream writeData:audioBuffer.mData maxLength:audioBuffer.mDataByteSize];
}

CFRelease(blockBuffer);
CFRelease(sampleBuffer);
먼저, 리더 아웃풋에서 나온 셈플 버퍼를 가져온다. 그리고나서 오디오 버퍼의 리스트를 얻기 위해 CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer 함수를 호출한다. 마지막으로 아웃풋 스트림의 각 오디오 버퍼를 쓴다(write).

이것이 우리는 처음에 하고자 했던 아이튠즈 라이브러리에 있는 음악을 스트리밍 한 것이다.(재생한건 아님) 이제 이 스트림 데이터를 어떻게 받아내는지 그리고 오디오를 어떻게 재생하는지 보자.


데이터 스트림
Multipeer Connectivity를 사용할 때 부터 NSInputStream 는 이미 만들어져 있었다. 먼저 우리는 데이터를 받기 위해 스트림을 해야한다.
// Start receiving data
// Start receiving data
inputStream.delegate = self;
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open]; 
이 클래스는  NSStreamDelegate 를 씌울 것이고, 우리는 이제 NSInputStream 으로부터 이벤트를 받을 수 있다.
@interface MyCustomClass () <NSStreamDelegate[CDATA[]]>
//...
@end

@implementation MyCustomClass
//...

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    if (eventCode == NSStreamEventHasBytesAvailable) {
        // handle incoming data
    } elseif (eventCode == NSStreamEventEndEncountered) {
        // notify application that stream has ended
    } elseif (eventCode == NSStreamEventErrorOccurred) {
        // notify application that stream has encountered and error
    }
}

//...
@end

위의 코드를 보면 델리게이트 메소드를 이용해서 스트림으로부터 이벤트를 받는다. 스트림이 끝나거나 에러가 나왔을 때, 앱에 알려야한다. 그래서 다음에 어떤 행동을 취할건지 정해야한다. 이제 우리는 이 이벤트로부터 얻은 스트림이 처리하여 가지고 있는 데이터에만 초점을 두면 된다. 우리는 이 데이터를 가져다가 사용하고, 그다음 Audio File Stream Services에 보내주는 작업이 필요하다.


스트림 파서
스트림 파서는 인코딩된 오디오 데이터를 넣고 디코딩된 오디오 데이터를 얻어오는 AudioFileStream 클래스이다. 먼저 AudioFileStream을 만들어보자.
AudioFileStreamID audioFileStreamID;
AudioFileStreamOpen((__bridge void *)self, AudioFileStreamPropertyListener, AudioFileStreamPacketsListener, 0, &audioFileStreamID);
우리는 클래스에 참조된 파서를 보내고, 프로퍼티는 콜백함수에 의해 바뀌고, 콜백함수에 의해 패킷들을 받는다. 우리는 이러한 기능을 하고 그 클래스에 참조되어 사용될 수 있는 콜백함수가 이 클래스 안에 필요하다.
void AudioFileStreamPropertyListener(void *inClientData, AudioFileStreamID inAudioFileStreamID, AudioFileStreamPropertyID inPropertyID, UInt32 *ioFlags)
{
    MyCustomClass *myClass = (__bridge MyCustomClass *)inClientData;
    [myClass didChangeProperty:inPropertyID flags:ioFlags];
}

void AudioFileStreamPacketsListener(void *inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, constvoid *inInputData, AudioStreamPacketDescription *inPacketDescriptions)
{
    MyCustomClass *myClass = (__bridge MyCustomClass *)inClientData;
    [myClass didReceivePackets:inInputData packetDescriptions:inPacketDescriptions numberOfPackets:inNumberPackets numberOfBytes:inNumberBytes];
}

didChangeProperty:flags: 메소드 안에서 다른 모든 프로퍼티가 준비됫다고 말해주는 kAudioFileStreamProperty_ReadyToProducePackets 프로퍼티를 찾고 있다. 이제 파서로부터 AudioStreamBasicDescription 를 가져올 수 있다. AudioStreamBasicDescription 는 오디오의 샘플비율(sample rate), 채널, 패킷당 바이트수 등등의 정보를 담겨있고 이것은 오디오 큐(audio queue)를 만드는데 꼭 필요한 요소이다.
AudioStreamBasicDescription basicDescription;
UInt32 basicDescriptionSize = sizeof(basicDescription);
AudioFileStreamGetProperty(audioFileStreamID, kAudioFileStreamProperty_DataFormat, &basicDescriptionSize, &basicDescription);
콜백으로 부터 받은 패킷에 사용될 다른 함수들은 나중에 오디오 큐 버퍼에 쌓일 디코딩된 오디오 데이터를 반환해줄것이다.

이제 스트림의 NSStreamEventHasBytesAvailable 이벤트로부터 인코딩된 데이터를 파서에 넣을 차례이다.
uint8_t bytes[512];
UInt32 length = [audioStream readData:bytes maxLength:512];
AudioFileStreamParseBytes(audioFileStreamID, length, data, 0);
파일 스트림은 파일의 타입을 알기게 충분한 바이트를 가질 때 까지 파싱을 할 것이다. At this point, it invokes its property changed callback with the property kAudioFileStreamProperty_ReadyToProducePackets. 그다음 이것은 우리가 사용하기 좋게 잘 포장된 디코딩된 데이터와 함께 해당 패킷이 받을 콜백을 호출한다.


오디오 큐
오디오 큐는 우리에게 오디오 버퍼를 생성하고, 채우고, 큐에 더할 수 있는것을 허락해주는 AudioQueue 클래스이다. 이것은 또한 재생, 일시정지, 멈춤등과 같은 오디오 컨트롤을 제공한다. 이제 큐와 버퍼를 생성해보자.
AudioQueueRef audioQueue;
AudioQueueNewOutput(&basicDescription, AudioQueueOutputCallback, (__bridge void *)self, NULL, NULL, 0, &audioQueue);

AudioQueueBufferRef audioQueueBuffer;
AudioQueueAllocateBuffer(audioQueue, 2048, &audioQueueBuffer);


오디오 큐를 만들기 위해서는 파서로부터 받은 theAudioStreamBasicDescription를 AudioQueueNewOutput 함수에 전달해 줘야하고, 시스템으로부터 호출된 콜백함수가 버퍼와 클래스 참조를 끝낸다. 다음으로 AudioQueueAllocateBuffer 함수를 호출하여 오디오 큐에 넘겨줄 수 있는 오디오 버퍼 한개를 만들고 잠시 멈추기 위한 버퍼의 사이즈도 함께 넘겨준다.


이제 파서가 패킷을 담은 콜백함수를 호출할 때까지 기다린다. 그리고 빈 버퍼를 패킷으로 채운다. 파서로부터 받을 수 있는 포멧은 VBR과 CBR 두가지 종류가 있는데, Variable Bitrate (VBR)는 비트율이 패킷이 어디있는지 따라 변할 수 있다는 것이고 Constant Bitrate (CBR)은 변하지 않는다(constant)는 것이다.

VBR의 경우,  많은 바이트를 가진 전체 패킷만을 버퍼에 채울 스 있다. 이것은 시스템에서 패킷을 보내주기 전까지는 버퍼가 차지 않는다는걸 의미한다. CBR의 경우, 패킷이 전송되는 도중에 버퍼를 가득 채울 수 있다.


CBR
AudioQueueBufferRef audioQueueBuffer = [self aFreeBuffer];
memcpy((char *)audioQueueBuffer->mAudioData, (constchar *)data,
또한 우리는 버퍼가 오버플로우가 되지 못하게 하거나 이것이 가득 차지 않았을 경우 기다리는 로직이 필요하다.

VBR
AudioQueueBufferRef audioQueueBuffer = [self aFreeBuffer];
memcpy((char *)audioQueueBuffer->mAudioData, (constchar *)(data + packetDescription.mStartOffset), packetDescription.mDataByteSize);

패킷이 버퍼에 넘처 남게되는 것을 체크하는 코드가 있다. 만약 다른 패킷의 mDataByteSize에 맞지 않는다면 우리는 다른 버퍼를 가져와야한다. 또한 패킷 디스크립션(packet descriptions)이 큐 되는동안 기다려야 한다.
바퍼가 차면, AudioQueueEnqueueBuffer 와 함께 시스템에 큐를 날린다.

AudioQueueEnqueueBuffer(audioQueue, audioQueueBuffer, numberOfPacketDescriptions, packetDescriptions);

이제 오디오를 재생할 준비가 끝났다. 모든 버퍼가 채워지고 큐 되면 AudioQueuePrime과 AudioQueueStart
를 사용하여 소리를 재생할 수 있다.
AudioQueueBufferRef audioQueueBuffer = [self aFreeBuffer];
memcpy((char *)audioQueueBuffer->mAudioData, (constchar *)(data + packetDescription.mStartOffset), packetDescription.mDataByteSize);

AudioQueueStart는 두번째 파라메터에 언제 재생될지에대한 시간을 나타내는 값을 NULL대신에 넣을 수 있다. 지금은 별로 중요하지 않으니 넘어가지만, 나중에 오디오 동기화(audio synchronization)을 하는데 꼭 필요한 것이니 기억해두면 좋다.

끝으로
이 글은 Multipeer Connectivity를 이용한 오디오 스트리밍에 대한 기초적인 글이다. 글을 마치면서 나는 조금더 복잡하고 잘 정리된 오픈소스 라이브러리를 민들었다. 좀더 자세한 내용을 알고싶으면, Github에 올라가있는 tonyd256/TDAudioStreamer 다듬어진 코드를 볼 수 있다.



 Tony DiPasquale  Developer

translate by canapio



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,