SeSAC_iOS_Day 58 | TIL

린다·2021년 12월 20일
0

SSAC_iOS_Dev

목록 보기
28/33
post-thumbnail

📂 Push Notification

✔️ 특정 화면으로 전환

  • 푸시의 종류에 따라 푸시를 클릭했을 때 뷰를 전환시킬 수 있다. 이때 가장 중요한 것을 항상 최상단 뷰가 무엇인지 파악하는 것. WindowRootViewController에 어떤 뷰컨이 놓여있는지 확인해야함
  • Scene은 SceneDelegate에서 관리하지만 사용자가 푸시를 눌러 앱을 실행시키는 life cycle을 관리하는 것은 AppDelegate에서 관리되기 때문에 SceneDelegate에서 window 객체를 AppDelegate로 가지고와서 코드를 구성해야함.
  • 먼저 Extension으로 현재 화면의 rootViewController가 뭔지 가져오는 코드를 작성해야함.
  • 만약 아래와 같이 코드를 작성하는 경우에는 만약 현재 ViewContorller 앞에 Navigation Controller나 TabBarController가 embed돼있는 경우, NavigationController 혹은 TabBarController가 리턴됨. 하지만 현재 View를 파악하고 위치를 이동시켜주기 위해서는 그 안에 있는 ViewController를 파악해야하기 때문에 더 자세하게 코드를 작성해줘야함
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController
  • 다음과 같은 메서드를 활용하여 현재 ViewController가 tabBar 혹은 Navigation인 경우 한 번더 메서드에 그 안의 viewController를 넣어 부르는 방식으로 현재 정확한 viewController를 파악할 수 있음
var topViewController: UIViewController? {
        return self.topViewController(currentViewController: self)
    }
    
func topViewController(currentViewController: UIViewController) -> UIViewController {
        if let tabBarController = currentViewController as? UITabBarController,
           let selectedViewController = tabBarController.selectedViewController {
            return self.topViewController(currentViewController: selectedViewController)
        } else if let navigationController = currentViewController as? UINavigationController,
           let visibleViewController = navigationController.visibleViewController {
               return self.topViewController(currentViewController: visibleViewController)
        } else if let presentedViewController = currentViewController.presentedViewController {
            return self.topViewController(currentViewController: presentedViewController)
        } else {
            return currentViewController
        }
    }
  • SceneDelegate의 window의 rootViewController의 embed된 viewController가 있는 경우 그 viewController까지 파악을 함
guard let rootViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController?.topViewController else {return}
  • rootViewController가 어떤 viewController인지에 따라서 보여주거나 이동해야하는 view가 달라지기 때문에 그러한 부분들을 처리해주면 된다.
if rootViewController is SnapDetailViewController {
            rootViewController.present(DetailViewController(), animated: true, completion: nil)
        }

✔️ 특정 화면에서만 푸시 받지 않기(Foreground)

  • Foreground 환경에서도 푸시를 보내주는 코드 내에서 현재 rootViewController를 위와 같이 파악한 후 종류에 따라 푸시 알림을 받지 않도록 설정해줄 수 있다.
guard let rootViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController?.topViewController else {return}
        if rootViewController is DetailViewController {
            completionHandler([])
        } else {
            completionHandler([.list, .badge, .sound, .banner])
        }

✔️ Remove Badge Number

  • 배지 갯수를 갱신해주는 것은 여러가지 상황이 있을 수 있는데 푸시 알림을 클릭하거나 앱을 실행시키는 등등 원하는 상황에서 갱신을 해줄 수 있다.
  • 앱을 실행시키거나 Foreground 상황에 들어가는 경우에 Badge Number을 지워주기 위해서는 아래와 같이 코드를 작성해주면 된다.
    func sceneWillEnterForeground(_ scene: UIScene) {
        UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
        UNUserNotificationCenter.current().removeAllDeliveredNotifications() // local, push 모두 해당
        UIApplication.shared.applicationIconBadgeNumber = 0
    }

✔️ Token

  • 앱을 삭제하거나 회원탈퇴를 하는 경우에는 토큰이 자동으로 삭제되고 다시 앱을 깔거나 회원가입을 하면 토큰이 갱신됨. 하지만 로그아웃과 같은 경우에는 Firebase에서 인식이 불가능하여 토큰 갱신이 자동으로 일어나지 않음.
  • FCM을 사용하는 경우, 고유한 기기를 구별해줄 수 있는 Instance ID, 토큰을 발급해준다. 이를 직접 제거해주고 나중에 런치됐을 때 새로운 토큰 정보가 날아오면 이 정보를 서버에 전달해주면 됨.
  • 별도의 서버를 사용하는 경우에는 Instance ID 대신 사용자를 구별해줄 수 있는 값에 대한 토큰을 가지고 있을 것 이기 때문에 이를 직접 제거해줘야함. 새롭게 토큰 정보를 받아오는 과정도 필요함.

📂 Codable

  • Codable은 Decodable & Encodable 프로토콜을 동시에 만족시키는 typealias
  • Encoding: Model -> JSON (ex. Realm을 사용한 백업, 복구를 진행할 때 JSON으로 파일을 변환시키는 경우)
  • Decoding: JSON -> Model
  • JSON은 예시일 뿐, 이 외의 외부 호환성을 위해 사용될 수 있음
  • 지난 프로젝트에서는 SwiftyJSON이라는 라이브러리를 사용하여 손쉽게 JSON파일을 사용할 수 있었으나 이번에는 Codable 프로토콜을 활용해보는 것이 목표!!

✔️ Decodable

  • 원하는 Model로 데이터를 변환시키는 과정
  • Class, Struct, Enum으로 변환이 가능하며 이 중 Struct를 가장 많이 사용함
  • 여러 Key들 중 일부만 디코딩하는 것은 가능.

아래와 같은 JSON 형식의 문자열이 있을 때

let json = """
{
"quote": "Your body is made to move so move it.",
"author": "Toni Sorenson"
}
"""

JSON 구조와 동일하게 구조체를 구성해주고 이로 변환할 때 Decoding을 사용할 수 있다. 이때 JSON key와 Struct의 변수명이 동일해야함!

struct Quote: Decodable {
    var quote: String
    var author: String
}

디코드를 하기 위해서는 Data 타입이 필요하기 때문에 현재 JSON형식으로 구성된 String을 utf8로 인코딩하여 Data로 변환시켜준다.

guard let result = json.data(using: .utf8) else { fatalError("Failed") }

그 후 JSONDecoder()를 사용하여 Data타입을 JSON으로 변환시켜준다. 이때 JSONDecoder의 특성을 설정해줄 수 있다.

let decoder = JSONDecoder()
//decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
    let value = try decoder.decode(Quote.self, from: result)
    print(value)
} catch {
    print(error)
}
  • 이때 .convertFromSnakeCase를 사용하면 아래와 같이 JSON Key와 Struct의 프로퍼티명이 snake case, Camel case로 다를 때 CodingKey를 따로 추가하지 않고 변환시켜줄 수 있다.
let json = """
{
"quote_content": "Your body is made to move so move it.",
"author_name": "Toni Sorenson"
}
"""

struct Quote: Decodable {
    var quoteContent: String
    var authorName: String
}
  • Encoding, Decoding을 할 때 key와 프로퍼티명이 동일해야하는데 아래와 같이 이름이 다르고 변경하지 않을 경우에는 CodingKeys를 사용하면 된다.
  • 열거형 CodingKeys에는 모든 속성이 포함되어있어야하기 때문에 필요한 키가 아니더라도 작성해줘야한다.
let json = """
{
"quote_content": "Your body is made to move so move it.",
"author_name": "Toni Sorenson"
}
"""

struct Quote: Decodable {
    var quote: String
    var author: String
    
    // 인코딩, 디코딩할때 사용할 키를 정의할 수 있음
    enum CodingKeys: String, CodingKey {
        case quote = "quote_content"
        case author = "author_name"
    }
}

☑️ init(from decoder: Decoder) throws

다음과 같은 JSON 형식의 문자열이 있다.

let json = """
{
"quote_content": "Your body is made to move so move it.",
"author": null,
"like_count": 12345
}
"""

이때 author의 값이 null 인 경우에는 "작자미상"이라는 데이터를 보여주고 like_count값이 10000이 넘는 경우에는 influencer에 true라는 값을 주고싶다면?

  1. let author: String? : author의 타입을 옵셔널로 설정하기
  2. let influencer: Bool이라는 프로퍼티 추가하기
  3. init(from decoder: Decoder) throws 초기화 구문을 통해 초기화 시켜주기
    3-1. let container = try decoder.container(keyedBy: CodingKeys.self)
    디코딩된 데이터는 내부 컨테이너에 저장되기 때문에 먼저 컨테이너에 접근해야함. key를 통해 각 값에 접근할 수 있음

    Returns the data stored in this decoder as represented in a container keyed by the given key type.

struct Quote: Decodable {
    let quote: String
    let author: String?
    let like: Int
    let influencer: Bool
    
    enum CodingKeys: String, CodingKey {
        case quote = "quote_content"
        case author
        case like = "like_count"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        quote = try container.decode(String.self, forKey: .quote)
        author = (try? container.decodeIfPresent(String.self, forKey: .author)) ?? "작자미상"
        like = try container.decode(Int.self, forKey: .like)
        influencer = (10000...).contains(like) ? true : false
    }
}
  • 이러한 과정을 거쳐 동일하게 디코딩하고 그 결과를 확인할 수 있다.
guard let jsonData = json.data(using: .utf8) else { fatalError("Failed") }
do {
    let value = try JSONDecoder().decode(Quote.self, from: jsonData)
    dump(value)
} catch {
    print(error)
}

✔️ Encodable

  • Encodable은 기존 구조체에서 JSON 형식으로 데이터를 변환시키는 과정에 사용한다.
  • 다음과 같이 구조체와 해당 구조체의 인스턴스를 담은 배열이 있다. 이 데이터들을 Encoder를 사용하여 JSON으로 변환시키고자 한다.
struct User: Encodable {
    var name: String
    var signUpDate: Date
    var age: Int
}

let users: [User] = [
    User(name: "Jack", signUpDate: Date(), age: 33),
    User(name: "Elsa", signUpDate: Date(timeInterval: -86400, since: Date()), age: 18),
    User(name: "Emily", signUpDate: Date(timeIntervalSinceNow: 86400 * 2), age: 11),
]
  • Decoder와 동일하게 인코딩을 진행할 때는 JSONEncoder()를 사용하면 된다. 이때 Encoder에 몇몇 설정을 해줄 수 있는데 print하는 format이라던지, date를 인코딩할때의 format 등을 설정해줄 수 있다.
let encode = JSONEncoder()
encode.outputFormatting = .prettyPrinted
encode.dateEncodingStrategy = .iso8601 // (iso 국제표준화기구)

do {
    let jsonData = try encode.encode(users)
    guard let jsonString = String(data: jsonData, encoding: .utf8) else { fatalError("Failed") }
    print(jsonString)
} catch {
    print(error)
}
  • Date의 경우 원하는 DateFormat이 있을 경우, DateFormatter를 활용하여 직접 설정해줄 수 있다.
let format = DateFormatter()
format.locale = Locale(identifier: "ko_KR")
format.dateFormat = "yyyy년 MM월 dd일 EEEE"
encode.dateEncodingStrategy = .formatted(format)

✔️ Meta Type

do {
    let value = try JSONDecoder().decode(Quote.self, from: result)
    print(value)
} catch {
    print(error)
}

위의 코드에서 Quote.self는 Meta Type이다.

  • Meta Type이란 클래스, 구조체, 열거형 등의 유형 자체를 일컫는 말이다.
    let quote: Quote = Quote(quote: "명언", author: "혜") 이때 quote의 타입은 Quote(구조체)이다. 이는 인스턴스에 대한 타입인 것 이다. 그럼 구조체 Quote의 타입은 무엇일까? 그 타입은 Quote.Type으로 이때 메타타입이 언급되게 되는 것이다!

  • 동일하게 let name: String = "린"에서 name은 String이기 때문에 name의 타입은 String.Type이 되는 것 이다.

  • 이때 구조체 User를 살펴보면 identifier는 타입 프로퍼티이다. User의 인스턴스인 currentUser에서는 name에만 접근이 가능하고 identifier에만 접근이 가능한데 그 이유는 타입 프로퍼티는 User라는 구조체 타입 자체에서만 접근이 가능하기 때문이다.

struct User {
    var name = "고래밥"
    static let identifier = 1234567 // 타입 프로퍼티
}

let currentUser = User()
  • 따라서 아래의 코드와 같이 Type 자체로 접근했을 때만 사용이 가능한 것 이다.
User.identifier
User.self.identifier
type(of: user).identifier

0개의 댓글