로컬 알림 처리하기

- 이전 포스팅에서 로컬 알림 발생하는 방법에 대해서 알아보았다

- 이번엔 해당 메세지를 처리하는 방법에 대해서 알아보겠다.

- 처리? 뭘 처리한다는거야? -> 휴대폰에 떠있는 이벤트 푸시 알림들을 터치해서 해당 앱을 들어가면 보통 홈 화면이 아닌 해당 이벤트를 공지해주는 화면으로 이동하게 된다. 이러한 동작을 하는 것이다.(물론 해당 푸시 알림들은 서버에서 보낸것이다)

 

쨋든...

알아보자

 

델리게이트 패턴

- UserNotification 프레임워크에서도 Delegate 패턴을 사용하여 처리한다.

 

//AppDelegate

//해당 동작을 위한 구현은 이전 포스팅 참고
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        if #available(iOS 10.0, *) {
            let notiCenter = UNUserNotificationCenter.current()
            notiCenter.requestAuthorization(options: [.alert, .badge, .sound], completionHandler: { _, _ in
            })
            //NotificationCenter의 대리자를 AppDelegate로 
            notiCenter.delegate = self
        }else{
            //9버전 이하
        }

        return true
   }

알림 메시지 클릭 이벤트를 AppDelegate가 감지할 수 있게 되었다.

그렇다면 어떻게 처리할지? 에 대해서 정의하겠다.

 

App이 실행되는 도중 알림이 오는 경우, userNotificationCenter(_:willPresent:withCompletionHandler:) 메소드가 호출된다.

따라서 이 메소드를 구현하면 우리는 앱 실행 중 알림이 도착했는지를 알 수 있다.

해당 메소드를 구현하지 않는다면, 앱 실행 중 도착한 메시지는 배너로 표시되지 않는다.

 

사용자가 알림을 클릭하면 userNotificationCenter(_:didReceive:withCompletionHandler:) 메소드가 호출된다

따라서 앱이 실행 중이든 아니든, 사용자가 클릭했다는 것을 알 수 있다.

 

알림이 도착했을 때

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        if notification.request.identifier == "firstPush"{
            let userInfo = notification.request.content.userInfo
            print(userInfo["name"]!)
        }
        //알림 배너 띄워주기
        completionHandler([.list, .badge, .sound])
    }
}

마지막에 작성된 completionHandler()는 반드시 호출돼야 한다.

이를 생략하면 앱 실행 도중에 알림 배너는 표시되지 않는다.

 

print(userInfo["name"]) 은 이전에 직접 작성한 userInfo를 확인하기 위함이다.

사용자가 어떤 알림을 클릭했는지 식별하기 위해서 사용한다.

 

사용자가 알림을 클릭했을 때

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        if response.notification.request.identifier == "firstPush"{
            let userInfo = response.notification.request.content.userInfo
            print(userInfo["name"]!)
        }
        completionHandler()
    }

위의 메소드와의 차이는 2번째 인자의 타입이 다르다는 것이다. 확인하고 사용하길 바란다.

NotificationCenter

애플 공식 문서에는 이렇게 적혀있다

A notification dispatch mechanism that enables the broadcast of information to registered observers.

등록된 Observer들에게 정보를 BroadCast할 수 있는 알림 발송 메커니즘

 

이전에 사용했을 때는 주로 키보드가 올라올 때, 내려갈 때 TextField가 가려지는 문제를 해결하기 위해서 사용한 경험이 있다.

오늘은 좀 더 자세히 알아보자


출처 : https://baked-corn.tistory.com/42

어떤 객체가 Post를 하면 Observe하고 있던 Observer들에게 BroadCast를 한다.

(....? Delegate랑 똑같은 거 아냐?)

맞다. 어떤 객체가 다른 객체와 소통/송수신 한다는 메커니즘은 같다

 

Delegate 패턴의 사용 예

protocol SomeDelegate{
	func sayName(name: String)
}

class FirstView: UIView{
	// 순환 참조 사이클을 막기 위해 강한 참조가 아닌 약한 참조로 선언한다
    // 이벤트를 발생할(책임을 전가하는) 타입에서는 Delegate를 프로퍼티로 선언한다
	weak var delegate: SomeDelegate?
    
    func tappedButton(){
    	delegate?.sayName(name: "tree")
    }
}

// 동작을 수행할 타입에는 Delegate를 채택한다
class SomeController: SomeDelegate{
	var view: SomeView = SomeView()
    
    init(){
    	view?.delegate = self
    }
    
    // SomeDelegate 채택 후, 준수
    func sayName(name: String){
    	print(name) // tree
    }
    
} 

크게 5가지만 생각하면 된다

  1. Delegate 패턴을 위한 Protocol 선언
  2. 책임을 전가할 타입(위임자)에서 delegate를 프로퍼티로 선언 -> 이때, 순환 참조 사이클을 막기 위해 약한 참조 사용
  3. 동작을 수행할 타입(대리자)에서 delegate를 채택
  4. 위임자에 있는 delegate를 대리자의 객체로 초기화
  5. 대리자에서 프로토콜을 준수

장점 : 코드를 이해하며 따라가기 쉬움

단점 : 많은 객체들에게 이벤트를 알려주는 것이 어렵고 비효율적

 

 

Notification

- NotificationCenter 라는 싱글톤 객체를 통해 이벤트들의 발생 여부를 옵저버에게 알림.

- Notification.Name 이라는 Key 값을 통해 Post/Observe 가능

 

class PostVC: UIViewController{
	@IBOutlet var button: UIButton!
    
    @IBAction func sendNotification(_ sender: UIButton){
    	NotificationCenter.default.post(name: Notification.Name("buyEmoticon"), object: nil)
    }
}


class ViewController: UIViewController{
	override func viewDidLoad(){
    	super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(sayHello(noti:)), name: Notification.Name("buyEmoticon"), object: nil)
        
    }
    
    deinit{
    	NotificationCenter.default.removeObserver(self)
    }
    
    @objc func sayHello(noti: Notification){
    	print("Hello") // Hello
    }
}

 

장점

  • 많은 줄의 코드가 필요없이 이벤트/동작을 구현할 수 있다
  • 다수의 객체들에게 동시에 이벤트의 발생을 알려줄 수 있음

단점

  • 추적이 쉽지 않음
  • post 이후 정보를 받을 수 없음

 

결론

  1. 가능하다면 Delegate로 구현하되, 하나의 이벤트로 다수의 객체에서 어떠한 동작을 해야하는 경우에만 NotificationCenter를 사용
  2. NotificationCenter를 사용해야 하는 이벤트들의 경우 NotificationCenter 사용

 

 

클로저

  • 제네릭, 프로토콕, 모나드 등과 결합이 가능하다
  • 람다와 비슷하다
  • 일정 기능을 하는 코드를 하나의 블록으로 모아놓은 것
  • 함수도 클로저의 일종

형태

  • 전역함수 : 이름 o, 값 획득 x
  • 중첩된 함수 : 이름 o, 다른 함수의 내부의 값 획득 o
  • 축약 문법으로 작성 : 이름 x, 주변 문맥에 따라 값을 획득 o

 

특징

  • 문맥을 통해 매개변수와 반환 값을 추론 가능하기 때문에, 생략이 가능하다
  • 단 한줄만 있다면 암시적으로 그것을 반환 값으로 취급
  • 축약된 전달인자 이름을 사용할 수 있다
  • 후행 클로저 문법을 사용할 수 있다

 

기본 클로저

정렬을 위한 함수 전달

func backWords(first: String, second: String) -> Bool{
	return first > second
}

var names: [String] = [.....]
let reversed: [String] = names.sorted(by: backWords) // 알파벳의 역순으로 정렬된 배열이 저장

정렬을 위한 클로저 전달

let reversed: [String] = names.sorted(by: { (first:String, second: String) -> Bool in
	return first > second
}) // 위의 코드보다 훨씬 간결해졌다.

후행 클로저

- 함수나 메서드의 마지막 전달인자가 클로저일 때, 소괄호를 닫고 작성할 수 있다.

let reversed: [String] = names.sorted(){ (first: String, second: String) -> Bool in 
	return first > second
}

//OR

let reversed: [String] = names.sorted{ (first: String, second: String) -> Bool in 
	return first > second
} // 소괄호 내에 어떠한 매개변수도 없기 때문에, 생략 가능

 

클로저의 간소화

- 클로저를 간소화 함으로써 가독성이 좋아지고, 코드가 간결해진다.

- 하지만 너무 많은 간소화를 통하면 소통이 되지 않을 수도 있으니 조심

 

문맥을 이용한 타입 유추

let reversed = names.sorted{ (first, second) in
	return first > second
} // 매개변수와 반환 타입을 생략 가능

단축인자 이름 사용

let reversed: [String] = names.sorted{
	return $0 > $1
} // $0 = first , $1 = second 라고 생각하면 된다

암시적 반환 표현

let reversed: [String] = names.sorted{ $0 > $1 }

아래로 내려오면서 코드가 훨씬 간결해지는 것을 볼 수 있다.

지금 예로 든 클로저는 간단해서 "어? 무조건 간소화하는 게 좋은 거 아냐?" 라고 생각할 수 있지만, 클로저 내부에서 많은 동작을 요구하고 매개변수 타입또한 복잡해지면 무턱대고 간소화하는 것은 오히려 역효과를 부를 수 있다

 

연산자 함수

- 연산자도 일종의 함수이다

 

// > 연산자 정의
public func ><T : Comparable>(lhs: T, rhs: T) -> Bool

T는 제네릭으로 이후에 자세히 포스팅할 것이다

여기서만큼은 T는 String이라고 생각하면 된다

 

우리는 위에서 reversed를 구하기 위해 (String, String) -> Bool 타입인 클로저를 사용했다

......

....?

> 연산자도 (String, String) -> Bool 타입이네?

 

그렇다, 클로저에도 타입이 존재하기 때문에, 매개변수의 클로저와 같은 타입의 클로저/함수는 매개변수로 전달할 수 있다

-> 쉽게 생각하면 된다.

var a: Int = 0 , var b: Int = 10  // 여기서 a, b 모두 Int지 않은가?

func printInt(num: Int){

    print(num)

} // 이 함수에서 필요한 매개변수인 num과 타입이 같은 a, b는 매개변수의 인자가 될 수 있는 것과 같다

 

다시 연산자 함수로 돌아가서

let reversed: [String] = names.sorted(by: >)

이렇게 간단하게 연산자 함수를 통해 작성도 가능하다

 

값 획득

- 클로저에서 개인적으로 가장 어려웠던 개념이다

- 이 자체가 어렵다기 보다는 다른 개념들까지 생각하며 염두하게 되면 헷갈리는 개념이기도 하다

- 값 획득을 통해 정의한 상수/변수가 더이상 존재하지 않아도 내부에서 그 값을 참조/수정이 가능

  1. 값 획득을 통해 정의한 상수/변수

  2. 존재하지 않아도

  3. 내부에서 그 값을 참조/수정이 가능

3가지로 나누어서 보자

 

//makeIncrementer 함수는 하나의 Int 매개변수를 받아서
// ()->Int 타입의 클로저를 반환한다

//incrementer 함수는 매개변수로 아무것도 받지 않고
// Int 타입을 반환한다

func makeIncrementer(amount: Int) -> (()->Int){
    var runningTotal = 0
    func incrementer() -> Int{//중첩된 함수
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let incrementByTwo = makeIncrementer(amount: 2)

let first = incrementByTwo() // 2
let second = incrementByTwo() // 4
let third = incrementByTwo() // 6

 

무언가 이상하다

incrementByTwo 라는 상수에 ()->Int 타입의 클로저를  반환 한 것은 알겠다.

근데 함수안에 있는 변수들은 함수가 종료될 때, 메모리에서 해제된다고 알고 있는데, 왜 값이 계속 증가해?

 

천천히 살펴보자

우선 incrementByTwo = makeIncrementer(amount: 2) 는 ()->Int 타입의 클로저라는 것을 잊지말라

1. first에서 incrementByTwo()를 호출했다

2. incrementByTwo() 내에 있는 runningTotal 변수가 생성된다.

3. incrementer 라는 함수가 생성된다.

4. incrementer 라는 함수가 반환된다.

5. incrementer() 함수가 실행된다.

6. runningTotal 값이 0에서 2로 증가한다

7. 2가 된 runningTotal이 반환된다.

 

이 순서인데.... 아니 4번에 incrementer라는 함수가 반환된 시점에서 runningTotal 변수는 죽은거아냐?

makeIncrementer하는 함수가 끝났잖아?

 

-> 맞는 말이다, 하지만 값 획득을 통해 외부의 값을 사용하는 것이다.

-> 위에서 3가지 단계로 나눠놓은 것을 빗대어 설명하자면

-> 1. 값 획득을 통해 정의한 상수/변수 : amount, runningTotal

-> 2. 존재하지 않아도 : makeIncrementer 함수가 종료되어서 메모리에서 사라져도

-> 3. 내부에서 그 값을 참조/수정이 가능 : incrementer하는 함수 내부에서 해당 값을 참조/수정이 가능

 

이러한 과정을 통해 runningTotal, amount 값을 사용할 수 있다.

 

 

클로저는 참조타입

- 이전 포스팅들에서 말했듯, 값 타입과 참조 타입이 존재한다

- 클로저는 참조타입이다.

- 만약 클로저가 값타입이라고 생각해보면 위의 incrementByTwo는 동작하면 안된다

  -> incrementByTwo 는 let 즉, 상수로 선언되었다.

  -> 상수로 선언된 값 타입 ?? struct!! 상수로 선언된 구조체 인스턴스의 내부 값을 변경할 수 없지 않았던가

  -> 근데 incrementByTwo는 상수인데 내부 값이 증가되는 것을 보면 클로저는 참조 타입이라는 것을 역으로 보일 수 있는 것이다

 

 

탈출 클로저

- 함수의 전달인자로 클로저가 전달되고, 해당 함수가 종료되고 클로저가 호출될 때, 클로저가 함수를 탈출한다고 표현한다

- 예를 들어 비동기 작업을 실행하는 함수들은 클로저를 ComplementHandler 전달인자로 받아온다.

  -> 비동기 작업으로 함수가 종료되고 난 후, 호출할 필요가 있는 클로저를 상요해야할 때 탈출 클로저가 필요하다

 

하지만 지금까지 알아본 함수들은 모두 @escaping 키워드를 찾을 수 없었다

 -> 모두 비탈출 클로저 였기 때문이다

 

typealias VoidVoidClosure: () -> Void

func withNoescapeClosure(closure: VoidVoidClosure){
	closure()
}

func withEscapingClosure(completionHandler: @escaping 
						VoidVoidClosure) -> VoidVoidClosure{
	return completionHandler
}

class SomeClass{
	var x = 10
    
    func runNoescapeClosure(){
    // 비탈출 클로저에서 self 키워드 사용은 선택
    	withNoescapeClosure{ x = 200 }
    }
    
    func runEscapingClosure -> VoidVoidClosure{
    // 탈출 클로저에서 self 키워드 사용은 필수!!
    	return withEscapingClosure{ self.x = 100 }
    }
}

let instance: SomeCLass = SomeClass()
instance.runNoescapeClosure()
print(instance.x) // 200

let returnedClosure: VoidVoidClosure = instance.runEscapingClosure()
returnedClosure()
print(instance.x) // 100


 

 

 

프로젝트가 내가 만든 프로젝트가 아니거나, 저장 공간을 옮겼을 때 나타나는 현상으로 보인다.

 

해결 방법!!

 

말 그대로 ..../경로에 Info.plist가 없어서 빌드가 되지 않는 것이다.

따라서 방법은 2가지

  1. 해당 경로에 Info.plist를 가지고 간다. -> 옮긴다
    1. 해보진 않았지만, 될 것 같다!!

1. Xcode 내의 프로젝트를 누른다

대충 이런 화면이 나온다

 

2. 좌측에 PROJECT와 TARGETS 중 TARGETS에 있는 자신의 프로젝트 이름을 클릭한다.

 

3. 뭐 Architectures, Assets, Build Locations ... 등 많은데 Packaging 을 찾는다!

 

4. 눈 뜨고 잘 보면 Info.plist라는 항목이 보인다.

 

5. 해당 항목에 있는 Value를 에러에서 나온 경로를 붙여준다.

 

 

6. 열심히 작업한다...

 

이 현상만 3번 겪었는데, 3번만에 작성하는 내가 웃기다...

이제는 그때그때마다 작성해놔야겠다

+ Recent posts