[Swift] Collection Type의 할당과 Copy

[updated 2014.7.9]

Xcode 6 beta 3 시기에 맞춰 Swift  Collection에 대대적 변경이 이뤄졌다. 아래는 Xcode 6 release notes 에서 발췌한 내용이다.

• Array in Swift has been completely redesigned to have full value semantics like Dictionary and
String have always had in Swift.  This resolves various mutability problems – now a ‘let’ array
is completely immutable, and a ‘var’ array is completely mutable – composes properly with
Dictionary and String, and solves other deeper problems.  Value semantics may be surprising
if you are used to NSArray or C arrays: a copy of the array now produces a full and
independent copy of all of the elements using an efficient lazy copy implementation.  This is a
major change for Array, and there are still some performance issues to be addressed.  Please
see the Swift Programming Language for more information.  (17192555)!

이제 Dictionary 와 Array 는 변수 할당시 (or 파라미터로 값 전달 시) 값이 항상 복제(copy)된다.

Collection의 아이템이 Reference Type인 경우, Reference Type이 참조하고 있는 값은 복제되지 않고 유지된다.

본 포스팅의 주제였던  Array의 특이사항들 – 값이 복제 되지 않고 전달되는 케이스 – 는 모두 없던 일이 되었다. Array 메소드인 copy(), unshare() 도 사라졌다.

Classes and Structures – The Swift Programming Language 에서도 Collection Assignment and Copy 섹션은 보이지 않는다.

복잡했던 Swift 의 Collection이 정리되었다. 이제 아래 내용들은 추억으로 남기자.

* 참고로 Swift 는 Reference Type 의 copy 를 권장하지 않는 듯 하다. class 객체를 복제할 일이 생기면, struct 이 더 적합한 것은 아니지 고민을 해보자. (관련한 Apple Developer Forum 글 링크 )

 

Swift의 Collection Type인 Dictionary 와 Array는 Structure로서 Value Type이다. (NSArray, NSDictionary 는 Class로서 Reference Type이다.)

같은 Structure Type 임에도 Dictionary와 Array 는 변수 할당시 (or 파라미터로 값 전달시) 값을 처리하는 로직에서 차이를 보인다.

Dictionary

Value Type 의 기본에 맞게 Dictionary는 새로운 변수(or 상수)에 할당이 되면 값이 복제(Copy) 된다.

Dictionary의 key or value 가 Reference Type (Class or Function) 이면, Reference Type이 참조하고 있는 값은 복제 되지 않고 유지된다. 

<del>// Reference Type Test Class</del>
<del>class TestClass{</del>
<del>    var testProperty = "objc"</del>
<del>}</del>

<del>var baseDictionay : Dictionary<Int, Any> = [1:"abc", 2:TestClass() , 3:"def"]</del>

<del>// 새로운 변수에 할당. copy 됨</del>
<del>var copiedTestDictionay = baseDictionay</del>

<del>// 1. Value Type 변경</del>
<del>baseDictionay[1] = "ghi" // value 값 변경</del>

<del>println("\(baseDictionay[1])") // print "ghi"</del>
<del>println("\(copiedTestDictionay[1])") // print "abc</del>

<del>// => 값 변경이 서로에게 독립적</del>

<del>// 2. Ref. Type 변경</del>

<del>// TestClass 객체의 프로퍼티를 변경</del>
<del>(baseDictionay[2] as TestClass).testProperty = "swift"</del>

<del>println("\((baseDictionay[2] as TestClass).testProperty)")  // print "swift"</del>
<del>println("\((copiedTestDictionay[2] as TestClass).testProperty)") // print "swift"</del>

<del>// => Ref.Type 값 변경은 서로 영향 미침</del>

 Array

Array는 Dictionary와는 다르게 변수에 값이 할당 되거나 함수의 파라미터로 전달 될때, 값이 복제되지 않고 그대로 전달(공유)된다.

Array 값의 copy 여부가 결정되는 시점은 할당(or 전달) 이 아니라, 그 후에 발생하는 값의 변경시점이다.

값의 변경이란 아이템 추가, 아이템 삭제, ranged subscript 를 이용한 값의 변경처럼 Array의 length에 영향을 미치는 동작을 의미한다.  (ranged subscript 란 [..] or […] 로 Array의 값을 변경하는 것으로 아이템 삭제가 가능)

Array 값의 Copy 처리는 Dictionay와 동일하다. (Array 아이템이 Reference Type 이면 Reference Type이 참조하는 값은 copy 되지 않고 공유된다.)

<del>var baseArray = [1,2,3,4,5]</del>
<del>var copiedArray_1 = baseArray // no copy</del>
<del>var copiedArray_2 = baseArray // no copy</del>

<del>//1. 단순 값 변경</del>
<del>baseArray[0] = 10</del>

<del>println(baseArray[0]) // print 10</del>
<del>println(copiedArray_1[0]) // print 10</del>
<del>println(copiedArray_2[0]) // print 10</del>

<del>// => copy 되지 않앗으므로, 변경 값이 모든 array에 영향</del>

<del>// 2. ranged subscript 이용한 값 변경</del>

<del>// baseArray의 값 변경 (실질 size 변경은 없지만 ranged subscript는 size 변경시킬 수 있는 능력을 소유)</del>
<del>// 결국 baseArray는 복제된 값을 가짐</del>
<del>baseArray[2..4] = [6,7,8] // copy</del>

<del>println(baseArray[3]) // print 7</del>
<del>println(copiedArray_1[3]) // print 4</del>
<del>println(copiedArray_2[3]) // print 4</del>

<del>// => copy되었으므로 다른 array에 영향 미치지 못함</del>

<del>// 남은 Array의 값을 변경</del>
<del>copiedArray_1[3] = 11</del>

<del>println(baseArray[3]) // print 7, 앞서 복제되었기에 독립적</del>
<del>println(copiedArray_1[3]) // print 11</del>
<del>println(copiedArray_2[3]) // print 11</del>
<del>// => copiedArray_1과 copiedArray_2 는 여전히 같은 값을 공유함</del>

Array 값을 변경하기 전에 명시적으로 값이 copy 되도록 할 수 있다.

unshare()와 copy() 메소드를 이용한다.

</del>
<del>// 위 코드 계속</del>

<del>copiedArray_1.unshare() // 독립선언</del>
<del>copiedArray_1[3] = 20</del>

<del>println(baseArray[3) // print 7</del>
<del>println(copiedArray_1[3]) // print 20</del>
<del>println(copiedArray_2[3]) // print 11</del>

<del>

unshare()는 return이 없는 함수다.

여러 변수가 하나의 Array를 공유하는 경우,  그 중 하나의 변수에서 unshare()를 호출하면 복제된 값을 변수가 갖는다. 만약 Array 값을 가지는 변수가 이미 하나라면 (unique 하다면) unshare() 메소드는 호출되더라도 값을 복제하지 않는다.

반면 copy()는 Array 값을 지니는 변수의 unique 여부와 상관없이 항상 값을 복제한다. 이런 행위에 맞게 copy()는 Array<T> 를 반환하도록 정의되어 있다.

<del>// 위 코드 계속</del>

<del>var copiedArray_3 = copiedArray_2.copy() // 명시적 copy</del>
<del>copiedArray_3[3] = 100</del>

<del>println(copiedArray_2[3]) // print 11</del>
<del>println(copiedArray_3[3]) // print 100</del>

궁금증

  • Array 를 이처럼 복잡하게 만들어 놓은 이유는 뭘까? Value Type 과 Reference Type이 애매하게 섞여있다.  어느 부분에 이점이 있는걸까?
  • 그럼 Dictionary 는 왜 Array 처럼 처리하지 않을까?
  • Array 나 Dictionary 가 copy 되어도 Reference Type의 값은 copy 되지 않는데, copy의 의미를 봤을 때 헷갈리지 않을까?
  • 온전히 독립적인 Collection을 만들려면 Reference Type 의 복제는 별도로 구현해야 하나?

 

* Reference

 

[Swift] Optional Type 그리고 ?

Optional의 존재는 nil 로부터 온다.

 nil 의미

Swift 에서의 nil 은 Objective-C 에서의 nil 과 다르다.

  • nil of Swift : 값이 없다  in 모든타입 (Reference or Value Type)
  • nil of Objective-C : 값이 없다 in Reference Type

즉, Swift에선 어떤 타입이든 값이 없으면  nil 이다.

Optional Type 

Optional Type 은 nil이 될 수도  있는 변수 (or 상수)를 의미하며 선언할 때 ‘?’ 를 사용한다.  ‘?’  이 없으면 Non-Optional Type 으로 nil이 될 수 없다.

// optional type.
var optionalString : String?   // nil 로 초기화

// non-optional type.
var nonOptionalString : String // nil 이 될 수 없음. 초기화 필요

WWDC 2014 에서 Safe, Power, Modern 세가지를 Swift의 특징으로 꼽았다. Optional은  Safe에 속한다.

기존 nil 로 인한 앱 크래쉬 문제를  nil 전용 변수 타입을 따로 정의해서 compile 시점에서 막아줄께 하는 것이다. (실제로 이것이 얼마나 안전한지는 더 써봐야 알겠다.)

개발자는 변수 선언할 때마다 Optional Type으로 선언할 것인지 판단해야 한다. 꼭 필요한 경우에만 Optional Type 사용을 권장한다.

Optional은 enum 으로 정의된다.

enum Optional<T>{
    case None
    case Some(T)
}

! (forced-unwrapping operator)

Optional을 이용하여 안전한 코드를 짜려면 반드시 if 문으로 값이 있는지 확인을 해야 한다. 값이 있는 경우엔 ‘!’ 를 이용하여 값이 있음을 명시적으로 알려줘야 한다.

‘!’ 는 forced-unwrapping operator 로서 애매모호한 Optional Type 값을 명확하게 들어낸다. 이를 통해 불필요한 Optional Type 추가 발생을 막는다.

let numCards = ["one" : 1 , "two" : 2, "three" : 3,
               "four" : 4 , "five": 5]

// dictionary[key] 는 optional type을 반환 (즉,myCard 는 Int?)
let myCard = numCards["two"]
if myCard { // 값을 체크

    let card = myCard! // 값이 있으므로 !로 값을 드러냄
    // card의 타입은 Int (Int? 가 아님)
   
    println("card \(card)")

}else {

    println("no card")
}

Runtime Error

Optional Type을 if 문 없이 unwrapping 하여 사용하면 Runtime Error가 발생한다. 더 이상 안전하지 않다.

let numCards = ["one" : 1 , "two" : 2, "three" : 3, "four" : 4 , "five": 5]
let myCard = numCards["nine"]    // myCard = nil 

println("\(myCard!)")      // Runtime Error 발생

 Optional Binding

결국 Optional Type을 다룰때는 항상

  1. 값 여부를 확인 하고
  2. 값이 있으면 unwrapping 하여

사용해야 한다.

그러나 매번 위처럼 단계적으로 처리하는 건 불편하다. Optional Binding은 이를 동시에 할 수 있다.

Optional Binding 포멧은 if let ~ 이다.

let numCards = ["one" : 1 , "two" : 2, "three" : 3,
               "four" : 4 , "five": 5]

if let card = numCards["two"] { // 값 체크 & unwapping 

    println("card \(card)")  // card 는 Int 타입.

}else{
    println("no card")
}

Optional Chaining

코드를 짜다보면 Optional Binding 이 중첩될 수 있다. 반복되는  if let~ 은  귀찮다.

여러 층의 Optional Binding을 한 줄에 쓰게 하는 것이 Optional Chaining이다. (Swift 개발팀에서도 Optional이 불편하긴 했나보다.)

Optional Chaining은 ‘?’ 기호를 사용한다. (Optional Type 선언에 사용하는 ‘?’ 과 동일시 하면 헷갈린다. 비슷하지만 다른것으로 보자.)

아래 예제 코드는 WWDC2014 에서 소개된 코드다.

// 중첩 Optional Binding
if let home = paul.residence {
    if let postalAddress = home.address{
        if let building = postalAddress.buildingNumber{
            if let convertedNumber = building.toInt(){
                println("building number of paul is \(convertedNumber)")
            }
         }
     }
}

// Optional Chaining
if let addressNumber = paul.residence?.address?.buildingNumber?.toInt(){

    println("building number of paul is \(addressNumber)")</pre>
}

코드를 간단히 설명하면 paul 은 class 개체로 Non-Optional Type 이고 나머지 residence, address, buildingNumber 는 Optional Type 이다.

paul 의 집 주소를 알기 위해 Optioanl Binding으로 순차적으로 값을 확인한다. 이를 Optional Chaining 을 이용하면 한 줄에 끝이다.

Optional Chaining을 철도 선로로 이해하면 쉽다. 값이 없으면 nil 행이고 값이 있으면 keep going. (WWDC2014 비디오 스크린샷을 올리기가 좀 그렇지만, 좋은 설명이라 일단 첨부한다.)

< Optional Chaining from WWDC2014 >

 

Optional 과 Enum Pattern Matching

앞서 살펴봤듯이 Optional 은 enum 타입이다.

enum Optional<T>{
    case None
    case Some(T)
}

따라서 enum을 이용한 Pattern Matching 이 가능하다.

let numberCards : Dictionary<String,Int> = [ "one" : 1, "two" : 2, "three": 3, "four" : 4,
                  "five" : 5, "six": 6, "seven" : 7]

for num in numberCards.keys{

    // Dictionary[key] 리턴은 optional type
    switch numberCards[num]{

        case .Some(let v) where v > 3 :
            println("\(v)")
        default:
          println("...")

    }

}

numberCards[num] 의 리턴값은 Optional Type 이므로 – 여기선 Int? – 위와 같이 switch, enum 을 이용하여 처리할 수 있다.

 

* Reference

[iOS8 Beta] TouchID (Local Authentication)

TouchID는 iPhone5S에 처음 적용된, 지문을 통한 사용자 인증 방식이다. 개발자의 접근이 막혀있던 TouchID 를 애플이 iOS8에서 오픈했다. (WWDC2014)

LocalAuthentication Framework

TouchID를 사용하려면 LocalAuthentication Framework을 import 해야 한다. Swift에선 Objective-C 처럼 Xcode Build Phases에서 Framework 추가할 필요도 없다. 그냥 적으면 된다.


import LocalAuthentication

iOS Simulator 에선 컴파일 되지 않는다. 본인은 이것땜에 시간을 조금 소비했다. (Swift가 생소해서 설정이든 뭐든 다른 문제가 있구나 생각했었다.)

Xcode6 beta3 부터 iOS Simulator 타겟으로 빌드는 된다. 그러나 LAContext 객체 생성부에서 Runtime Error가 발생한다.

NSDebugDescription=The connection to service named com.apple.CoreAuthentication.daemon was invalidated.

LAContext class

API 사용은 심플하다. LAContext Class 메소드 두 개만 사용하면 되다.  ( iOS8 Pre-Release Reference에 친절하게도 Objective-C  코드가 적혀있다.)

  1. canEvaluatePolicy(_:error:) – TouchID 가능 여부 판별
  2. evaluatePolicy(_:localizedReason:reply:) – TouchID 인증 결과 리턴

func authenticateUser(){

    let myContext = LAContext()
    var authError : NSError?
    let localizedReasonText = "본인 인증 하세요."

    if myContext.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error:&authError) {
        myContext.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics,
                                 localizedReason:localizedReasonText,
                                 reply:{(success: Bool, error : NSError! )in

            if (success) {
                // 인증 성공
                self.didUserAuthenticated()

            } else {
                // 인증 실패
                self.didUserAuthenticationFaild(error)

            }
        })

    }else{
       // TouchID 사용 불가
       self.didUserAuthenticationFaild(authError!)
    }
}

func didUserAuthenticated(){

    println("user authenticated")
}

func didUserAuthenticationFaild(error:NSError){

    switch error.code{

        case LAError.AuthenticationFailed.toRaw(): // limit exceed or Biometric lockout
            println("failed")

        case LAError.UserCancel.toRaw():
            println("canceled by user")

        case LAError.UserFallback.toRaw():
            println("user wants to enter password")

        case LAError.SystemCancel.toRaw():
            println("canceled by system")

        case LAError.PasscodeNotSet.toRaw() , LAError.TouchIDNotAvailable.toRaw(), LAError.TouchIDNotEnrolled.toRaw():
            fallthrough
        default:
            println("TouchID is not available")

    }
}

LAError 

에러상황은 LAError로 구분한다. LAError 정의는 아래와 같다.


enum LAError : Int {
   case AuthenticationFailed
   case UserCancel
   case UserFallback
   case SystemCancel
   case PasscodeNotSet
   case TouchIDNotAvailable
   case TouchIDNotEnrolled
}

evaluatePolicy(_:localizedReason:reply:) 를 호출하면 TouchID 인식요청 알림창이 나타난다.

< TouchID 알림창 >

사용자가  ‘암호입력’ 버튼을 탭하면 리턴되는 것이 UserFallback 이다. 이 때 암호는 디바이스 잠금 암호가 아니다. 앱 내부에서 처리하면 된다. 아래같이 커스텀 뷰컨트롤러를 띄우는 것도 하나의 방법이겠다.

    ...    
   .case UserFallback:
        self.performSegueWithIdentifier(passwordSegue, sender: nil)
    ....   

AuthenticationFailed 는 지문 인식 실패를  의미한다.

알림창에서 인식 3회 실패하거나 연속 5회 실패로 TouchID 가 잠길때 리턴된다. (TouchID 잠김은 아이폰 잠김화면에서 TouchID 반복 실패시 암호를 직접 입력해야 하는 상황으로 생각하면 된다.)

앱내에서 TouchID 잠기면 디바이스 전체에 영향을 미친다.  즉 디바이스가 잠길 경우  TouchID 대신 암호를 입력해야한다.

 

살펴본것처럼 API 사용은 어렵지 않다.

디바이스 잠금번호를 알면 지문 등록이 가능하니, 앱 내에서 사용하는 비밀번호를 TouchID 로 대체하는 것이 안전하다고만 할 수 없다.

애플이 TouchID 사용하는 경우엔 디바이스 비밀번호를 4자리 숫자로 설정하지 못하도록 제한할 수도 있지 않을까? (그렇게 했으면 좋겠다.)

참고로 iOS8에서는 KeyChain에 저장된 데이터에 사용자가 접근할 때 사용자에게 TouchID 인증을 요청할 수 있다. 개발자의 TouchID API 호출없이 Attribute에 키값을 추가하면 OS가 처리해주는 방식이다.

 

* Reference

  • Local Authentication Framework Reference (iOS8 Pre-Release Library)

[iOS] Push Notification – 앱 삭제 여부 판별이 가능한가?

‘앱이 Push Notification(푸쉬 알림)을 정상적으로 수신 가능한지 서버에서 판별할 수 있을까?’ 란 의문이 들었다. (‘앱에서 푸쉬 알림을 정상적으로 수신했느냐?’ 와는 다름)

개발 서버에서 APNS (Apple Push Notification Service)에 푸쉬 알림을 신청하기 전(또는 직후)에 디바이스로의 전달여부를 파악할 수 있다면, 실패시 사용자에게 다른 액션을 취할 수 있을 것이다.

APNS가 전송을 100% 보장하지 않음을 알고 있었지만, 혹시나 하는 맘에 iOS Developer Library를 다시 한번 들춰 보았다.

검토한 결론은 아래와 같다.

  • 앱이 삭제된 경우는 확인할 수 있다. (Feedback 서비스)
  • 앱에서 Push Notification 수신을 허가한 후, 사용자가 설정에서 알림 받기 거부로 설정한 경우는.. 명확치 않다. (Feedback 서비스를 통해 식별 가능한지 테스트가 필요하다. 안될 것 같긴 하다. )
  • 실제 디바이스에 푸쉬 알림이 도착했는지에 대한 여부는 알 수 없다.
  • 디바이스가 장시간 꺼져 있거나 잠시 꺼져있는 동안 여러 개의 푸쉬 알림이 온 경우,  알림이 유실될 수 있다.

이와 관련한 iOS Developer Library 문서 내용을 이번 포스팅에서 정리한다. (서버를 이용한 실제 테스트는 차후 해볼 예정이다.)

Push Notification

기본적으로 Push Notification 은 단방향 서비스이다. 개발 서버에서 APNS 에 푸쉬 알림을 요청하면 APNS가 알아서 디바이스에 전송한다. 개발 서버는 APNS에 요청하면 할 일은 끝이다.

 

< from iOS Developer Library >

개발 서버는 APNS 에 푸쉬 알림을 요청할 때, 데이터 format 등에 대한 정상 여부는 리턴 받지만 실제 도착여부에 대해서는 리턴을 받지 못한다.

앞에서도 언급했듯이 APNS 는 100% 전송을 보장하지 않는다. 개발문서에서 기술하고 있는 유실 케이스는 디바이스 전원이 꺼져있는 경우다. APNS의 QoS (Quality of Service) 컴포넌트는 디바이스 전원이 꺼져있는 경우, Notification을 잠시 저장했다가 다시 사용 가능해질 때 Notification이 전달되도록 하지만, 다음의 경우 저장된 Notification이 폐기된다.

  • 잠시 꺼져 있는 경우(얼마나 잠시인지 명시되지 않았음) , APNS는 Notification을 저장을 하는데 저장 가능한 Notificaiton은 하나다. 디바이스가 꺼졌있는 동안 하나의 앱에서 여러 개의 푸쉬 알림을 보낸다면, 제일 나중에 보내진 Notification만 저장되어 전달된다. 이전에 저장된 Notification 은 폐기된다.
  • 장시간 꺼져 있는 경우 (장시간이 얼마나인지는 명시되지 않았음), 저장된 Notification은 폐기된다.

개인적으로 아이폰 베터리가 다되어 충전 후 다시 켰을 때, 하나의 앱에서 여러 개의 푸쉬가 전달되는 경험을 한 것 같다. 애플 내부에서 개선이 되고 있는데 여전히 100% 장담이 안되어 개발 문서를 업데이트 하지 않은 건지, 아니면 본인의 착각인지 이것도 테스트 해봐야 겠다.

Feedback Service

The feedback service gives providers information about notifications that could not be delivered—for example, because the target app is no longer installed on that device. For more information, see “The Feedback Service.”

위 개발 문서의 내용과 같이 Feedback 서비스는 Notification 전달 실패 정보를 개발 서버에 알려주기 위한 서비스다. APNS는 Feedback 서비스를 이용한다. (Feedback 서비스를 통해 불필요한 푸쉬 요청을 피하고 전반적인 Performance 향상을 얻을 수 있다.)

푸쉬 알림이 앱이 삭제된 이유로 전달되지 못했다면 , APNS 는 이를 Feedback 서비스에 알리고 Feedback 서비스는 실패한 디바이스의 토큰을 리스트에 저장한다. APNS 에서 푸쉬 알림을 디바이스로 전송하기 전에 실패한 – data format error 등  – 경우에는 Feedback 서비스의 리스트에 등록되지 않는다. (APNS는 실제로 디바이스에 Notification을 전달해보고 앱의 삭제 여부를 파악하는 듯하다.)

몇 번이고 확인해봤지만, 사용자가 Push 접근을 허락한 이후 설정에서 이 기능을 disabled 한 케이스에 대해선 언급이 없다. 실제로 ‘알림센터에서 보기’를 끄더라도 알림 타입이 ‘None’이 아닌 다른 것 – 배너 등- 으로 선택되어 있다면 푸쉬 알림을 받는다. 앱의 푸쉬 알림에 대한 모든 설정을 전부 Off 로 해놔야 특정 앱에 대한 푸쉬 알림을 받지 못하는데, 이 경우에 Feedback 리스트에 등재가 될지 테스트를 해봐야겠다.

개발 서버는 APNS 연결과는 별개로 Feedback 서비스에 접속해서 디바이스 토큰 리스트를 요청해야 한다. Feedback 서비스는 리스트가 요청될 때마다 리스트를 전달한 후 리스트를 초기화(clear) 한다.

Feedback 서비스로부터 받은 리스트의 데이터에는 timestamp 정보가 포함되어 있다. 이를 이용하여 리스트에 있는 디바이스 토큰이 Push Service에 재등록되어 있는지를 확인할 수 있다. (푸쉬 알림을 보낸 시각과 디바이스 토큰의 timestamp 를 비교하면 디바이스 토큰의 재등록 여부를 확인할 수 있을 것 같다.)

< Binary format of a feedback, from iOS Developer Library >

Feedback 서비스로의 요청을 위한 url, port 및 format 등의 내용은 The Feedback Service를 참고하자.

개발 문서에는 적어도 하루에 한번 Feedback 서비스를 확인하라고 나왔는데, 서비스마다 Push Notification 이용처가 다르니 필요에 따라/용도에 맞게 확인 주기를 결정해야 할 것 같다.

삭제된 앱이 삭제되기 전 디바이스에 남은 최후의 push-enabled 앱이였다면, 앱이 삭제되는 순간 디바이스와 push service 간의 커넥션이 끊겨버려 Feedback 서비스에 추가되지 않는다고 한다. (아마 개발폰일 경우에나 간혹 발생할 일일 것 같다.)

마무으리

몰랐던 내용을 알아서 좋긴 한데, 찾고 있던 답은 아닌 것 같다. 좀 더 고민해보고 테스트해서 결론을 정리하자.

* Reference

 

[iOS] Permission Alert View 에 custom 메시지 추가하기

iOS App이 사용자의 사진, 캘린더, Push Notification, 연락처 등 개인정보에 접근 시도할 때,  System은 AlertView를 띄어 사용자에게 접근 권한을 부여할 것인지 묻는다.

이 때 보여지는 AlertView 메시지는 context 정보가 없기 때문에 사용자의 허가를 득하기가 쉽지 않다.

그래서 요즘은 권한 부여 AlertView를 띄우기 전에 접근이 필요한 이유를  사용자에게 먼저 알려주는 방법을 사용함으로써 이러한 장벽을 낮추려는 시도가 많다.

오늘 이와 관련한 글  The Right Way to Ask Users for iOS Permissions 을 읽다가 한가지 놀라운 – 나만 몰랐으니 나만 놀란걸 수도 – 사실을 발견하였다. (링크된 블로그 글도 꼭 읽어보길 바란다.)

글에 첨부된 이미지들을 살펴보니 System에서 관장하는 접근 권한 AlertView에 App에 특화된 접근 목적이 기술되고 있는 것 아닌가.

실제 구현 방법을 조사해보니 너무 간단했다. 간단히 방법을 알아보자. (너무 간단해서 개인 블로그 대기 리스트에 넣지 않고 바로 쓴다.)

구현 

iOS 는 info.plist의 특정 키값을 통해 접근 권한 AlertView에 메시지를 추가 할 수 있게 한다. 즉 개발자가 할 일은 info.plist에 키 값을 넣고 메시지를 넣으면 끝이다. (다국어를 지원한다면 Localized 작업 필요)

iOS Developer Library 의 Information Property List Key Reference 를 보면 접근 권한 메시지에 해당하는 키값을 확인할 수 있다. Xcode의 editor를 이용한다면 “Privacy – ” 로 시작하는 키값을 찾으면 된다. Key 값에 따라 지원하는 iOS 버전도 다르니 Refernce를 확인하자. (애석하게도 Push Notification 에 대한 키값은 보이질 않는다.)

< From : iOS Developer Library >

간단한 테스트 앱을 만들어 확인하였다. Info.plist 에 키값을 추가 하기 전의 접근 권한 알림창 – 기본 접근 권한 알림창 – 은 아래와 같다.

< 메시지 적용 전 접근 알림 >

아래와 같이 info.plist에 키값을  추가하였다. (사진과 연락처)

< info.plist 화면 in Xcode >

앱을 실행시키면 기존 접근 권한 알림과 달리 info.plist에 설정한 메시지가 AlertView에 추가된 것을 확인할 수 있다.

< 사진 라이브러리 접근 알림 창 >

< 연락처 접근 권한 알림 창 >

 

App에서 접근하려고 하는 사용자의 데이터가 App 동작의 핵심 역할을 한다면, 접근이 필요한 이유를 명시함으로써 사용자의 행동을 좀 더 쉽게 유도할 수 있을 것이다.

본인은 위 내용을 현재 작업 중인, 아니 현재 뿐 아니라 앞으로 작업할 서비스에도 적용할 생각이다.

iOS6, iOS7에서 사용자의 화면캡쳐(Screenshot) 감지하기

불현듯 Snapchat이 사용자의 화면 캡쳐를 어떻게 감지하는지 궁금해졌다.  구글링 및 간단한 데모를 통해 알아본 바를 정리한다.

iOS 디바이스는 홈 + 전원 버튼을 같이 누름으로써 디바이스에 보여지고 있는 화면을 사진으로 앨범에 저장할 수 있다.

사용자의 이러한 행동을 코드 내에서 감지할 수 있는 방법은 iOS 7 전후가 다르다.

iOS 7 이전 버전에서는 사용자가 스크린 캡쳐를 하면 화면의 touch 행위가 취소된다. touch가 취소되면 touchesCancelled:withEvent: 메소드가 호출되는데 이를 이용하면 사용자의 스크린 캡쳐를 감지할 수 있다.

Snapchat에서 사진을 보려면 화면을 계속 누르고 있어야 한다. 화면을 계속 누르는 행위를 하고 있기 때문에, 사진을 보는 중에 스크린 캡쳐가 발생하면 touch 취소 이벤트가 발생하게 된다.

iOS 7에서 사용자의 스크린 캡쳐는 touch에 아무런 영향을 미치지 않도록 바뀌었다. 따라서 위에서 설명한 방식으로는 스크린 캡쳐를 감지할 수 없다. 대신 Apple은 UIApplicationUserDidTakeScreenshotNotification을 별도로 정의하여 좀 더 명확하게 사용자의 스크린 캡쳐를 감지할 수 있도록 하였다.

UIApplicationUserDidTakeScreenshotNotification

Posted when the user presses the Home and Lock buttons to take a screenshot.

This notification does not contain a userInfo dictionary. This notification is posted after the screenshot is taken.

Availability

  • Available in iOS 7.0 and later.

Declared In

UIApplication.h

아래는 확인을 위해 작성한 데모 코드 중 일부다. (싱글뷰 프로젝트를 하나 만들었다. 기본으로 생성되는 UIViewController의 내부 코드다.)

- (void)viewDidLoad{

[super viewDidLoad];

// Do any additional setup after loading the view from its nib.

    // iOS7 판단
    if(floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1){

       // UIApplicationDidTakeScreenshotNotification을 등록, iOS7만 지원
       [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUserTakenScreenShot:) name:UIApplicationUserDidTakeScreenshotNotification object:nil];

    }

}

// NSNotificationCenter에 등록한 selector
// UIApplicationUserDidTakeScreenshotNotification이 발생하면 호출됨
-(void)didUserTakenScreenShot:(NSNotification*)noti{

    // 간단히 alert을 띄움
    [self showScreenShotAlert];

}

-(void)showScreenShotAlert{

    UIAlertView *alert = [[UIAlertView alloc]initWithTitle:nil message:@"User did take screen shot" delegate:nil cancelButtonTitle:@"ok" otherButtonTitles:nil, nil];

    [alert show];

}

// 화면을 누르면 색을 변화시킴
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{

    self.view.backgroundColor = [UIColor yellowColor];

}

// iOS7 이전에서 화면을 누르고 있는 중에 스크린 캡쳐가 발생하면 호출됨
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{

    // 색상을 검은색으로 바꿈. 디바이스에 저장되는 사진도 막을 수 있는지 확인해보기 위해 배경 색상을 변경
    self.view.backgroundColor = [UIColor blackColor];

}

// 화면에서 손을 떼면 호출됨.
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{

    // 실수로 손을 뗀 후 스크린 캡쳐를 하게 되는 경우를 확인하기 위해 별도의 색상을 지정.
    self.view.backgroundColor = [UIColor whiteColor];

}

테스트 코드는 간단하다. (iOS6와 iOS7 모두 테스트)

iOS 7에서 스크린 캡쳐를 하면 didUserTakenScreenShot:이 호출되고 alert이 발생한다. touchesCancelled:withEvent:는 호출되지 않는다.

iOS 6에서는 화면을 누르면 배경색이 노란색으로 변경되고, 손을 떼지 않은 상태에서 스크린 캡쳐를 하면 배경색이 검정색으로 바뀌면서 검정색 화면이 앨범에 저장된다. 만약 UIImageView가 있다면, 배경색을 검정색으로 바꾸는 대신 removeFromSuperview를 통해 이미지가 저장되는 것을 막을 수 있다.

iOS 6에서는 이처럼 화면이 저장되기 전의 화면을 컨트롤 할 수 있지만 (테스트 결과 100%는 아닌 것 같다. 가끔 노란색 화면이 앨범에 저장되기도 하였다.) , iOS 7에서는 화면이 그대로 앨범에 저장된다. UIApplicationUserDidTakeScreenshotNotification이 스크린캡쳐 후에 호출되기 때문이다. iOS 7에서 캡쳐되는 화면을 변경하는 방법은 아직 없는 듯 하다. (아직 발견되지 않은 듯 하다.)

* Reference

[iOS] 능력자의 블로그를 통해 되돌아본 ARC

즐겨찾기 해놓은 iOS 개발 블로그를 둘러보다 ARC에 관한 좋은 글을 발견하였다. 글 내용 중 본인이 몰랐던 또는 잊고 있었던 내용 중심으로 일부를 정리한다.  (원문 블로그 링크 )

이전에 본인이 포스팅한  [iOS] Automatic Reference Counting보다 훨씬 실용적인 내용이 많이 담겨져 있으므로 꼭 원문 블로그 링크의 글을 읽어보길 권한다.

Toll-Free Bridging

ARC는 Objective-C의 Object만을 대상으로 한다. Core Foundation과 같은 C/C++ 기반의 코드는  ARC 범주에서 벗어나 있다.

어플리케이션 개발에 Objective-C 클래스만을 이용할 수도 있겠지만, 때에 따라 Core Graphics와 같은 Core Foundation이 필요한 경우도 있다.

앞서 언급했듯이 Core Foundation은 ARC의 범주가 아니므로 Core Foundation API를 사용할 때는 개발자가 수동으로 메모리 관리를 해야한다. 그러나 다행히(?)도 Core Foundation 중 일부 클래스는 Objective-C  클래스로의 타입 캐스팅을 통해 Objectiv-C 객체처럼 사용가능하며 ARC에게 메모리 관리 책임을 맡길 수 있다. (이런 호환 가능한 클래스를 toll-free bridged type이라 한다. 리스트 참고)

Objective- C → Core Foundation이든 그 반대든, toll-free bridged type이 타입 캐스팅을 할 때는 compiler에게 메모리 관리 책임에 관한 정보를 알려줄 필요가 있는데 이때 사용하는 지시어가 __bridge, __bridge_transfer, __bridge_retained 이다.

1) __bridge

Objective-C Object인 NSObject 타입을 잠시동안만 Core Foundation 타입으로 변환하고 싶을 때 사용한다. ARC가 메모리 해지의 책임을 갖고 있다.


-(void)testMethodWithString:(NSString*)text{
...
... CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)text, NULL,  .....);
...
}

위 코드의 CFURLCreateStringByAddingPercenctEscapes 메소드의 두번째 파라미터는 CFStringRef 타입이고 이는 NSString과 호환되는 toll-free bridged type이다.

text는 testMethodWithString: 함수의 파라미터로 strong 성격을 지닌다. 함수의 시작점부터 끝날 때까지는 text 객체는 해지되지 않는 것을 보장받는다. Core Foundation API인 CFURLCreateStringByAddingPercenctEscapes를 사용하면서도 이러한 속성은 유지가 되어야 하며 CFStringRef로 변환되더라도 ARC가 text 객체의 메모리 관리 책임을 갖는다.

2) __bridge_transfer

Core Foundation에서 NSObject로 타입 캐스팅 하는 경우 사용한다. 보통 생성된 Core Foundation 객체는 CFRelease()를 이용하여 메모리 해지를 해줘야 하는데, NSObject 타입으로 바뀌었으니 메모리 해지는 ARC가 담당해라 라는 의미다.


NSString *temp = (__bridge_transfer NSString*)CFURLCreateStringByAddingEscapes(...);

CFURLCreateStringByAddingEscapes(…); 메소드는  CFStringRef타입을 반환한다. CFStringRef는 NSString로 타입 변환이 가능하며, 보통 NSString으로 다루는 것이 개발자에게 편리하다. NSString 타입으로 변환하여 반환하는데 이때 ARC에게 메모리 관리 책임은 이제 너라고 알려주는 역할을 __bridge_transfer가 한다.

CFBridgingRelease()라는 helper 함수가 존재하는데 위 코드는  NSString *temp = CFBridgingRelease(CFURLCreateStringByAddingEscapes(…))과 동일하다.

CFBridgingRelease라는 이름은 CFURLCreate…로 Core Foundation 객체를 생성했으니 이와 쌍으로 release 한다는 의미를 가진다. 보통 Create, Copy, Retain으로 명명된 Core Foundation 함수를 호출할 때 생성 객체가 toll-free bridged type이면 CFBridgingRelease()를 사용하면 된다.

3) __bridge_retained

NSObject 타입을 Core Foundation타입으로 캐스팅하는 경우 사용한다.  __bridge와 다른 것은 메모리 해지의 책임이 ARC로 유지되는 것이 아니라 ARC의 책임이 아니게 된다. 즉 Core Foundation 타입으로 변환된 객체에 대한 메모리 해지의 책임은 개발자에게 있으며 CFRelease()를 통해 수동으로 관리를 해야 한다.

...
CFStringRef s2 = (__bridge_retained CFStringRef)s1; // s1은 NSString 타입
...

CFRelease(s2); //명시적으로 메모리 해지 필요

CFBridgingRetain()이라는 helper 함수가 존재하는데 위 코드는 CFStringRef s2 = CFBridgingRetain(s1); 과 동일한 의미를 가진다. CFRelease()와 쌍을 맞추기 위해 Retain()이 들어갔다고 이해하면 된다.

__bridge는 Core Foundation간의 변환에만 사용되지는 않는다. 어떤 API는 void* 를 파라미터로 갖는 경우도 있다. void* 파라미터에는 Objective-C object, Core Foundation object등 어떠한 타입도 들어갈 수 있다는 의미다. (Objective-C 내에서 사용되는 id 타입과 헷갈리지 말자.)

// objective-c => void*
MyClass *myObject = [[MyClass alloc] init];
[UIView beginAnimations:nil context:(__bridge void*)myObject];
...

// *void -> objective-c
-(void)animationDidStart:(NSString*)animationid context:(void*)context{
...
MyClass *myObject = (__bridge MyClass*)context;
...

}

Strong

  • strong 속성을 지닌 property와 Instance variable을 사용할 경우, 변수가 더 이상 필요하지 않는 시점에 변수를 nil로 할당하는 것이 좋다. (개인적으로 자주 까먹는다. nil처리를 하지 않았다고 어플이 쉽게 죽거나 하진 않기 때문).
  • UIViewController 안에 strong 변수가 있는 경우 viewDidUnload() 메소드에서 nil 처리를 하면 UIViewController가 deallocated 되기 전에 불필요한 메모리 사용을 막을 수 있다.

read-only 속성

Property로 선언된 변수인 경우 self.name  같이 항상 Property 방식으로 접근하는 것이 좋다. property로 선언된 변수를 property’s backing instance ( .m 파일에서 프로퍼티 변수를 _name과 같은 형식으로 접근할 수 있다.)로 접근하는 경우는  init 메소드 또는 custom getter/setter 메소드로 한정하라.

@property (nonatomic, strong, readonly) NSNumber *temperature;

헤더파일에 위와 같이 readonly로 정의된 property인 경우, @implentation 에서 _temperature를 통하여 값을 변경한다면 ARC는 이상한 버그를 낼 수도 있다.

이 경우 좋은 솔루션은 header 파일의 선언은 그대로 두고 .m 파일에서 class extension을 이용하여 @property(nonatomic, strong, readwrite) NSNumber *temperature 로 재정의 하여 사용하는 것이다.

IBOutlet 

  • IBOutlet property에 권장하는 속성은 weak이다.
  • nib 파일에서 최상위 view(보통 self.view)가 IBOutlet으로 정의한 view를 소유하고 있으므로 weak 지시어로 생성하여도 무방하다.
  • IBOutlet을 weak으로 선언하는 가장 큰 이점은 viewDidUnload()메소드에서 nil처리를 하지 않아도 된다는 점이다. (최상위 view가 파괴될 때 자동으로 nil 처리가 된다. viewDidUnload시점엔  nil 이 되어 있다.)
  • iOS 디바이스가 low-memory warning을 받으면 view가 unload되는데 이때 weak으로 처리해야 IBOutlet에 연결된 view도 자동으로 해제된다.

그 밖의 정보 & Tip

  • LLVM 3.0 컴파일러를 도입하면서 .m 파일 (@implementation 내부)에 instance variable 설정이 가능해졌다.
  • MRC 에서 개발자가 @property(nonatomic, retain)을 즐겨 사용하는 이유는 메모리 관리를 편하게 하기 위해서였다. 프로퍼티에 retain 키워드를 넣음으로써 변수 할당때마다 retain (or copy) 키워드를 넣을 필요가 없다. ARC에서 이는 더이상 특별한 이점이 아니다. ARC에선 다른 클래스에서 사용할 경우에만 property를 이용하는 것이 좋다.
  • copy property : NSString 또는 NSArray에 copy property를 사용하는 경우가 많다. 이는 프로퍼티에 할당하더라도 기존의 값이 변경되지 않길 바라는 경우에 사용한다. copy를 하면 할당된 객체와 동일한 객체를 새로 만들어서 변수에 할당한다. copy는 strong과 동일한 성격을 지닌다.

== updated 2013/7/25  ==

확인해보니 iOS 6.0부터 viewDidUnload 메소드가 deprecated 되었다. iOS 6 부터는 low-memory 상태에서 viewDidUnload가 호출되지 않는다.

< from iOS developer library >

위에서 정리된 내용 중 viewDidUnload()가 언급된 곳은 IBOutlet 과 strong property 두 곳이다.

  •  IBOutlet : 권장 프로퍼티가 weak라는 것에는 문제는 없어 보인다. low-memory warning과 별개로 nib파일의 최상위 view에서 이미 IBOutlet으로 선언한 view를 소유하고 있으므로, UIViewController에서 다시 strong 속성으로 view를 소유하는 것은 효율적이지 않아 보인다.
  • strong 변수의 nil 처리 : viewDidUnload는 low-memory warning에서만 호출되도록 되어 있다. 그러나 더이상 low-memory 상태에서 호출되지 않으므로 didReceiveMemoryWarning()에서 현재 뷰에 사용되지 않는 strong 변수에 대하여 nil처리를 하여야 한다. strong 변수를 property로 선언하고 getter메소드에 nil인 경우를 처리해 놓는다면, didReceiveMemoryWarning()에서 nil로 할당된 변수가 다음에 사용될 경우 간편하게 처리될 수 있을 것 같다.

UIScrollView 컨텐츠 영역 화면 캡쳐하기

Bitmap image context(이전 포스팅 참조)를 활용한 화면 캡쳐를 해보자. 단순 화면 캡쳐가 아닌 UIScrollView내의 컨텐츠를 이미지로 캡쳐하여 저장한다.

테스트를 위해 화면 아래로 스크롤이 가능하도록 20개의 row를 가진 UITableView 하나를 만든다. UITableView는 UIScrollView를 상속하고 있어 별도의 UIScrollView를 만들지 않았다.(구현 모습은 아래 그림 참고)

테이블 row 를 클릭하면 UIScrollView영역, 즉 20개의 row가 담긴 이미지가 앨범에 저장되도록 한다.

test project

바로 코드를 통해 확인해보자.

// import QuartzCore
#import <QuartzCore/QuartzCore.h>

- (void)viewDidLoad{

    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    _tableView.delegate = self;
    _tableView.dataSource = self;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{

    // scroll이 생기도록 20개의 row를 생성
    return  20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    // 간단하게 row를 출력하는 UITableViewCell 생성
    UITableViewCell *cell = nil;
    cell = [tableView dequeueReusableCellWithIdentifier:@"test"];
    if(!cell){
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"test"];
    }

    cell.textLabel.text = [NSString stringWithFormat:@"%d", indexPath.row];
    return cell;
}

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{

     // capture 이미지 생성
    UIImage* captureImage = [self captureContentsInScrollView:indexPath.row];

    if(!captureImage) return;

    // 사진 앨범에 저장
    UIImageWriteToSavedPhotosAlbum(captureImage, nil, nil, nil);

}

-(UIImage*)captureContentsInScrollView:(int)selectRow{

    // 생성된 이미지를 저장할 변수
    UIImage *capture = nil;

    // UIScrollView의 기존 frame을 저장 (이 예제에선 UITableView가 UIScrollView의 역할)
    CGRect originTableViewFrame = _tableView.frame;

    // capture할 영역을 지정. UIScrollView의 컨텐츠 사이즈
    CGSize captureSize = CGSizeMake(_tableView.contentSize.width, _tableVie.contentSize.height);

    // bitmap graphic context 생성
    UIGraphicsBeginImageContextWithOptions(captureSize, YES, 0.0);

    // UIScrollView의 frame을 content 영역으로 변경
    _tableView.frame = CGRectMake(0, 0, captureSize.width, captureSize.height);

    // UIScrollView frame영역을 bitmap image context에 그림
    [_tableView.layer renderInContext:UIGraphicsGetCurrentContext()];

    //  bitmap graphic context로부터 이미지 획득
    capture = UIGraphicsGetImageFromCurrentImageContext();

    // UIScrollView의 frame을 원래대로 변경 (변경하지 않으면 스크롤이 안됨)
    _tableView.frame = originTableViewFrame;

     // bitmap image context 종료
     UIGraphicsEndImageContext();

    return capture;
}

Simulator로 실행하여 row를 선택하면 다음의 이미지가 앨범에 저장되는 것을 확인할 수 있다.

전체 캡쳐 결과

UIImageWriteToSavedPhotosAlbum()함수는  UIKit에서 지원하는 함수로 디바이스 앨범에 이미지를 저장한다. delegate를 구현하면 이미지 저장 여부를 어플리케이션에서 알 수 있다. (여기선 생략)

위 (UIImage*)captureContentsInScrollView:(int)selectRow 함수에 파라미터로 선택된 row를 전달하고 있다. 코드에서는 selectRow 파라미터가 사용되지 않았으나, 이 파라미터와 cell의 높이를 활용하여 captureSize를 조정하면 선택된 셀까지만 캡쳐되도록 할 수 있다. 아래 그림은 적용한 결과 화면이다.

선택한 row까지만 캡쳐

[iOS] Bitmap Image Context

대부분의 iOS 개발서적은 UIGraphicsBeginImageContext()를 이용한 화면 캡쳐 내용을 포함하고 있다. 그러나 아쉽게도 본인이 봤던 개발서에는 UIGraphicsBeginImageContext()의배경 설명 없이 스킬 위주로만 설명이 되어 있어 이를 활용하는데 어려움이 있었다. 이번 포스팅에서는 Bitmap Image Context에 대해 간단히 정리해본다.

iOS developer library의 Drawing and Creating Images 에 다음과 같은 설명이 있다.

Most of the time, it is fairly straightforward to display images using standard views. However, there are two situations in which you may need to do additional work:

두 번째 항목으로부터  bitmap image context의 용도를 파악할 수 있다.  화면에 바로 나타나지 않는 – offscreen – 이미지를 생성하고 싶을 때는 반드시 bitmap image context를 생성해야 한다.

bitmap image context를 생성하는 함수가 바로 UIGraphicsBeginImageContext()다. 화면 캡쳐를 생각해보면, 화면을 캡쳐한 이미지는 화면에 보이진 않고 내부적으로 생성되어 사진앨범에 저장된다.

UIKit에서 지원하는 UIGraphicsBeginImageContextWithOptions( CGSize size, BOOL opaque, CGFloat scale ) 함수는 iOS 4.0 이후부터 지원되며 각 옵션은 다음을 나타낸다.

  • size : bitmap context 사이즈. 생성될 이미지 사이즈와 연관있다. 
  • opaque : 생성될 이미지의 투명도 여부. 생성할 이미지가 투명도(alpha)를 가지고 있으면 NO, 불투명하면 YES. ( YES 의 퍼포먼스가 더 높다.)
  • scale : 0.0 이면 디바이스의 화면에 맞게 이미지가 결정된다. 개발자가 임의의 값을 넣을 수도 있다.

예를 들어 UIGraphicsBeginImageContextWithOptions(CGSizeMake(100.0,100.0), NO, 2.0) 으로 생성되는 이미지의 크기는 200 x 200으로 size 값에 scale 을 곱한 값이다.

iOS 4.0 이전에 지원했던 UIGraphicsBeginImageContext(CGSize size) 함수는 UIGraphicsBeginImageContextWithOptions 에서 scale = 1.0 인 것으로 레티나 디스플레이에서는 UIGraphicsBeginImageContext()로 생성된 이미지가 만족스럽지 않을 수도 있다.

UIGraphicsBeginImageContext를 이용한 이미지 생성은 보통 다음의 단계를 따른다.

-(void)createBitmapImage{

    // 1. bitmap context를 생성하고 이를 graphics stack에 넣는다.
    UIGraphicsBeginImageContextWithOptions(imageSize, NO, [UIScreen mainScreen].scale);

   // 2. UIKit 또는 Core Graphics 루틴을 활용하여 bitmap context에 이미지를 그린다.
   // ex) [UIImage drawInRect:], [CALayer renderInContext:] 등

    // 3. bitmap context로부터 이미지를 받아온다.
    UIImage *createdImage = UIGraphicsGetImageFromCurrentImageContext();

    // 4. bitmap context를 graphics stack으로부터 빼낸다.(bitmap context를 종료한다.)
    UIGraphicsEndImageContext();
}

(1)번에서 생성하는 bitmap context의 크기 – CGSize parameter 값 – 과 (2)에서 그릴 이미지 크기가 같아야(당연히 비율도 맞아야) 이미지 왜곡이 생기지 않는다. 만약 그릴 이미지 크기가 bitmap 크기보다 크다면 image가 짤린 것처럼 나올 수도 있다.

* Reference : iOS Developer Library – Drawing and Creating Images