UIViewController

역할

  • 데이터의 변경에 대한 응답을 View에 Update한다.
  • View와 User와의 상호 작용에 응답한다.
  • View의 크기 조정 및 Layout 관리
  • 다른 객체(viewController 포함)를 조정한다.

간략하게, View와 밀접하게 연결되어 있다.

class UIViewController : UIResponder

또한, UIViewController는 UIResponder를 상속받았다. UIResponder에 대한 이야기는 밑에서 다시 할 것이다.

우선은 SuperView/SubViews 들과의 응답자 체인으로 엮여있다.
이것은 즉

1. 자신의 event의 처리를 SuperView에 passing할 수 있다.

2. 자신의 SubView들의 event를 자신이 처리할 수 있다.

가 가능하다.

 

대부분의 애플리케이션 내에는 여러개의 ViewController가 존재하고 사용된다.

어떤 VC(ViewController)는 리스트를 나타내는 테이블뷰 하나만을 관리할 수 있고,

다른 VC는 리스트, 회원창, 친구목록 등 다양한 View를 관리할 수 있다.

 

UIResponder

앞선 블로그 코코아 프레임워크에서 말했듯, 이 또한 클래스로 NSObject를 상속받는다.

class UIResponder : NSObject
  • Event 처리에 중요한 역할을 한다.
  • 여러 메소드와 next라는 프로퍼티를 사용하여 부모에게 event를 전달하거나, 동시 처리도 가능해진다.
    • touchesBegan
    • touchesMoved
    • touchesEnded
    • touchesCancelled
  • next라는 프로퍼티를 사용하여 다음 responder에게 이벤트를 전달 가능

responder 라고는 주로 becomeFirstResponder() / resignFirstResponder() 를 사용한 기억이 많다.

TextField를 사용하며 키보드를 올리고 내리고 하기 위해 사용해왔지만, 정확히 어떻게 동작하는 지에 대해서는 생각해보질 못했다.

키보드가 올라오기 위해서는 TextField가 window의 첫번째 Responder가 되어야 한다.

메소드이름 그대로 TextField가 첫번째 Responder가 되기 때문에, 키보드가 올라오며, 반대로 Resign하면 내려간다.

 

View 관리

  • 각 VC는 View들이 포함되는 RootView를 관리한다.
  • RootView는 주로 나머지 뷰 계층 구조에서 Container 역할을 한다.
  • VC의 View들은 lazy하게 메모리에 Load된다.
    • 선언한다고 메모리에 적재되는 것이 아닌, 사용할 때(즉, 호출될 때) 메모리에 적재된다.
    • VC 생성 시의 생명주기

StoryBoard를 사용할 때

스토리보드 내에서 1. VC로 사용할 View 와 2. VC 를 지정(연결)한다.

주로 간단한 화면 구조의 애플리케이션이나 혼자서 작업할 때 사용되는 방법이다.

// 스토리보드를 통한 VC 초기화
let newVC = self.storyboard?.instantiateViewController(withIdentifier: "VC이름")

 

Nib  파일을 사용할 때

Nib파일을 사용하여 VC에 대한 View를 지정한다.

스토리보드를 사용하지 않기에, 스토리보드 내에서 가능했던 VC간의 Segue나 관계를 사용/정의할 수 없다.

초기화 방법 : init(nibName:bundle:)을 사용해서 초기화한다.

 

이후의 View들의 Load

VC의 생명주기 메소드들 중 가장 첫 단계인 loadView() 메소드를 사용하여 나열된다.

이후에 DidLoad, willAppear, DidAppear ... 

 

VC의 크기(RootView의 크기)

VC의 RootView는 항상 할당된 공간에 크기가 맞춰진다. 또한 RootView 내에 있는 SubView들은 Interface Builder를 사용하여 각 뷰와 SuperView와의 관계를 통해 크기, 위치 등이 지정된다.

 

구현 방식

1. AutoLayout - View 사이의 관계, 제약조건 등을 통하여 크기, 위치 등을 설정하며, 기기 크기에 대해 대응 가능

2. Frame - CGSize(크기), CGRect(위치)를 직접 할당한다.

 

 

View 관련 Notification

위에 사진으로 나타낸 VC의 생명주기의 메소드이름을 통해 View들이 어떤 상태인지를 알 수 있다.

반대로 말하면, View들의 현재 상태를 VC를 통해 알 수 있다.

 

UIKit

iOS를 공부하고 진행하다보면 코코아(Cocoa) 라는 단어를 여러번 접하게 된다.

코코아 터치, 코코아 프레임워크 등...

 

Cocoa

- NSObject를 상속받는 모든 클래스를 가르키는 단어

 

Cocoa Touch

- 코코아 터치 프레임워크란 iOS 환경을 구축하기 위한 최상위 프레임워크이다.

- 일반적으로, 옵씨에서의 NSObject 와 스위프트에서 사용되는 UIKit / Foundation 등에 포함되는 대부분의 클래스가 여기에 속한다.

  - 따라서 iOS 애플리케이션은 코코아 터치 프레임워크 환경에서 구축된다고 할 수 있다.

UIKit

- UIKit 프레임워크는 UI를 관리하고, 사용자와의 이벤트(Interaction)을 처리해주는 프레임워크다.

  - macOS에서는 AppKit이라는 프레임워크를 사용한다고 한다.

- 처리하는 주된 이벤트들은 제스처(터치, 스와이프, 핀치... 등), 애니메이션, 이미지, 텍스트 등 말 그대로 UI와 연결되는 것들이다.

- UIView, UIViewController 등의 앞에 UI가 붙는 클래스들 또한 UI를 위해 사용되는 것이고 당연히 UIKit 프레임워크에 속한다.

 

Foundation

- 애플리케이션의 중심을 담당한다.

- 원시 데이터 타입(String, Int, Double 등)이 Foundation 프레임워크에 포함되어있기 때문에, 반드시 사용된다.

- Resource, Notification, Error, FileSystem, URL 등 애플리케이션을 동작하기 위해 필요한 대부분이 속해있다.

 

 

그렇다면 UI를 작성하기 위해 UIKit을 import하고 동작을 위해 Foundation을 import해야 하는가??

기본적인 코코아 프레임워크의 계층구조이다.

UIKit은 최상단의 Cocoa Touch 계층이고 Foundation은 CoreServices 계층에 속한다.

그렇기 때문에, UIKit을 import하면 Foundation을 별도로 import하지 않아도 된다.

init(frame: CGRect)

  • Interface Builder에서 쓰이지 않고, 코딩(Programmatically)으로 UIView를 상속받은 클래스를 만들 때 사용
  • ex) let sampleButton = UIButton(frame: CGRect(x: 3, y: 0, width: 100, height: 200))

required init?(coder: NSCoder)

  • Interface Builder에서 생성되는 초기화 구문

 

Interface Builder란 간단하게 말해서는 xib, nib, storyboard를 사용하여 VIew를 작업하는 것!

 

View나 Button과 같은 것들을 커스텀하는 경우, 위 두가지의 초기화 구문이 필수

override init(frame: CGRect) {
	super.init(frame: frame)
	setupView()
}

required init?(coder aDecoder: NSCoder) {
	super.init(coder: aDecoder)
   	setupView()
}

 

 

awakeFromNib()

  • Interface Builder에서 객체가 init(coder:)로 초기화된 후 호출됨
  • IB에서 inspector와 같은 방법으로 값을 조정하면 위 커스텀 클래스는 아카이브되어 있다가 언아카이브됨(xib파일로)
  • init 시점에서는 frame과 관련된 크기, 위치 등 뷰가 완벽하게 만들어지지 않은 상태

awakeFromNib()와 init()에서 모두 설정해야 하는 이유

뷰를 만들고 커스텀 뷰를 IB에서 연결했을 땐, init(frame:), init(coder:)와 awakeFromNib() 구준데 모두에서 설정을 해준다

경험상 init(coder:) 시점에는 frame 이나 Layer 관련없는 값들, -> frame정보가 없으므로

awakeFromNib 시점에는 frame 이나 Layer 관련된 값들을 설정하도록 구현하면 됩니다.

init(frame:) 은 이미 frame 정보가 있으니까 괜찮구요.

물론 autoLayout 처리를 하는 경우는 awakeFromNib 보다 더 뒤에 해야합니다.

 

Codable이란?


A type that can convert itself into and out of an external representation.

자신을 변환하거나 외부 표현(JSON 같은 데이터를 주고 받는 형식)으로 변환할 수 있는 타입이다.

 

또한

Codable은 Decodable과 Encodable을 동시에 채택한 타입이다.

 

Decodable : 자신을 외부표현(external representation)에서 디코딩 할 수 있는 타입

Encodable : 자신을 외부표현(external representation)으로 인코딩 할 수 있는 타입

 

따라서, Codable은 자신을 외부표현으로 디코딩 & 인코딩이 가능한 타입 이라고 생각하시면 될 것 같다

 

Codable은 프로토콜

Codable은 프로토콜이므로 Class, Struct, Enum 에서 모두 사용할 수 있습니다. (이전 Protocol 포스팅에 있음)

그리고 타입에서 Codable을 채택했다는 것은 해당 타입을 serialize / deserialize 할 수 있다는 것

 

struct SomeType{
	var name: String
    var id: Int
}

이라는 구조체가 있다고 가정하자.

해당 타입에 Codable을 채택해보자

struct SomeType: Codable {
	var name: String
    var id: Int
}

이제 SomeType이라는 구조체는 외부표현(JSON형식)으로 변환이 가능한 타입입니다.

 

Encode

JSON으로 만드는 방법

//인코더 생성
let encoder = JSONEncoder()

//인스턴스 생성
let someInstance = SomeType(name: "First", id: 1)

//인코딩해서 jsonData에 저장 
// 1. encode메소드의 리턴타입은 Data
// 2. encode메소드는 throws -> try? 사용 -> try? 사용으로 jsonData는 옵셔널
let jsonData = try? encoder.encode(someInstance)

//Data 형식을 보기 좋게 String으로 변환
if let jsonData = jsonData, let jsonString = String(data: jsonData, encoding: .utf8){
	print(jsonString) // {"name" : "First", "id" : 1}
}

 

주로 주고받을 데이터 형식을 약속할 때, JSON을 사용한다. 하지만 어느 곳을 가도 {"name":"First","id":1} 처럼 가로로 쭉 늘어놓은 데이터로 약속을 하지 않을 것이다. 보통 봐온 JSON형식은 아래와 같다

{
	"name" : "First",
    "id" : 1
}

위처럼 JSONString을 이쁘게 만들 수 있다

//위와 같이 이쁘게 만들어준다
encoder.outputFormatting = .prettyPrinted

//key값을 기준으로 오름차순으로 정렬한다
encoder.outputFormatting = .sortedKeys

//그렇다면 나는 key값을 기준으로 오름차순으로 정렬한 이쁜 데이터를 받고싶어!
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

 

Decode

JSON을 내가 만든 타입으로 변환하기

주로 서버에 있는 데이터(JSON)을 받아서 내가 저장하고 싶은 타입의 모델에 저장하는 작업을 자주하게 된다.

이때 사용할 수 있는 것이 Codable이다.

 

//디코더 생성
let decoder = JSONDecoder()

//위에서 만든, Encode된 JSON을 Data로 변환
var data = jsonString.data(using: .utf8)

//Data 형식을 내가 만든 모델인 SomeType의 인스턴스로 변환
if let data = data, let someInstance = try? decoder.decode(SomeType.self, from: data){
	print(someInstance.name) // First
    print(someInstance.id) // 1
}

 

 

CodingKey

음... 쉽게 예를 들어보자

struct SomeType: Codable{
	var name: String
    var id: Int
}

위와 같은 타입에 Codable을 채택했다.

근데 넘어오는 데이터 형식이

{
	"name" : "First",
    "identifier" : 1
}

이렇게 넘어온다고 하자

 

내가 생성한 모델의 id와 넘어오는 JSON의 identifier의 "Key"가 다르다 -> 이렇게 되면 

No Value associated with key id 라고 나온다.

 

해결 방법은 2가지가 있다.

  1. JSON형식에 맞춰 모델의 프로퍼티 명을 작성하는 방법
    1. 오류는 없겠지만, 가독성 측면에서 좋지 않다
  2. CodingKey 사용!

CodingKey 사용

struct SomeType: Codable{
	var name: String
    var id: Int
    
    enum CodingKeys: String, CodingKey{
    	case name
        case id = "identifier" // identifier라는 키는 이제부터 id프로퍼티가 담당할거야!
    }
}

 

 

지금까지 Swift에서 모델에서 JSON으로 인코딩, JSON에서 모델로 디코딩 방법과

key값이 다를 때 사용하는 CodingKey에 대해 알아보았다.

 

 

 

 

 

 

 

 

+ Recent posts