'?' 키워드를 사용하는 Optional은 변수의 값이 nil일 수 있다는 것을 표현
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
var justInteger: Int
var optionalInteger: Int?
옵셔널은 타입중 하나이다.
이름이 있는 타입의 뒤에 ?를 붙여 만들 수 있는데, 해당 타입의 값이 있을 수도 있고 없을 수도 있다는 뜻의 타입이다.
enum으로 한 번 감싸져 있어서, 값이 들어있는지 옵셔널 바인딩이나, 강제 언래핑을 사용해서 값에 접근해야 한다.
구조체란, 인스턴스의 값(프로퍼티)을 저장하거나, 기능(메소드)를 제공하고 이를 캡슐화할 수 있도록 스위프트가 제공하는 타입.
구조체는 struct 키워드로 정의.
스위프트의 기본 타입은 모두 구조체로 구현되어 있다.
이는 기본 데이터 타입은 모두 값 타입이라는 의미이다.
따라서, 전달인자를 통해 데이터를 전달하면 모두 값 복사되어 전달될 뿐, 함수 내부에서 아무리 전달된 값을 변경하려고 시도해도 기존의 변수나 상수에는 전혀 영향 미치지 못한다.
// 프로퍼티 이름(name, age)으로 자동 생성된 이니셜라이저를 사용하여 구조체를 생성합니다.
var yagomInfo: BasicInformation = BasicInformation(name: "yagom", age: 99)
yagomInfo.age = 100 // 변경 가능!
yagomInfo.name = "Bear" // 변경 가능!
subscript(index: Int) -> Int {
get {
// Return an appropriate subscript value here.
}
set(newValue) {
// Perform a suitable setting action here.
}
}
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
// TimesTable구조체의 multiplier를 3으로 설정
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// "six times three is 18" 출력
Class, Struct, Enum에서 collection, 순열, list, sequence 등 집합의 특정 멤버 요소에 쉽게 접근하기 위한 방법이다.
추가적인 메소드없이 특정 값을 할당(set)하거나 가져(get)올 수 있다.
Swift의 String은 Struct타입이고, characters의 collection이다. 즉 Array\의 element가 Character인 배열이다.
하지만 Swift에서 String은 subscript, 즉 str[0]같이 Int로 접근하지 못하고, String.index로 접근해야한다.
그 이유는 Swift에서 Character는 1개 이상의 Unicode Scalar로 이루어져 있다. 즉 크기가 가변적이다. 아래와 같이 하나의 이모티콘은 여러개의 unicodeScalars로 이루어져 있는 것을 확인할 수 있다.
---> 참고
따라서 String은 subscript를 Int가 아니라 String.Index를 통하여 값을 확인할 수 있다.
예를 들어 "h" 의 경우, UTF-8로 인코딩하면 길이가 1을 차지하지만, "홍" 이라는 문자의 경우, 3을 차지한다.
이처럼 문자열 내부의 각 문자가 늘 동일한 바이트 수를 차지하는 것이 아니기 때문에, Int 형식의 Subscript를 사용해서 특정 위치에 있는 문자에 접근하는 것은 불가능하다.
따라서 Swift에서 문자열 내의 각 Character는 언어 마다 그 크기가 가변적일 수 있기 때문에 Subscript 대신 index를 사용하여 올바른 위치에 접근할 수 있다.
class Human {
let name: String = "unknown"
var age: Int = 0
}
struct Person {
let name: String = "unknown"
var age: Int = 0
}
class 객체를 let으로 선언한다면,
let human : Human? = Human()
human?.name = "aa" // error
human?.age = 13 // ok
human이란 클래스 인스턴스를 생성할 때 let으로 한단 것은
실제 힙 영역에 저장된 저장 프로퍼티 name, age와는 상관 없이
스택 영역에 저장된 human 안의 주소값이 상수로 설정되는 것이다.
클래스의 경우, 인스턴스 생성 당시 let으로 선언하든 var로 선언하든
클래스의 저장 프로퍼티에 접근하는 것엔 아무 영향을 주지 않는다.
대신 human 이란 스택 상수는 0x11111111이란 인스턴스의 값을 가지고 있고, 이 human이란 상수 안의 값을 변경하는 것에 영향을 준다!
human = nil //error
human2 = Human()
human = human2 // error
반면에, 구조체 인스턴스를 let으로 선언한다는 것은?
let person : Person? = Person()
person?.name = "aa" // error
person?.age = 13 // error
person = nil // error
그러면 구조체 인스턴스를 var로 선언한다는 것은?
var person : Person? = Person()
person?.name = "aa" // error
person?.age = 13 // ok
person = nil // ok
class Contacts {
var email: String = ""
var address: String = ""
init() { print("Contacts Init") }
}
class Human {
var name: String = "unknown"
var contacts: Contacts = .init()
}
원래 클래스, 구조체 모두 인스턴스를 생성할 경우
초기화 구문(initializer)이 불리는 순간 모든 프로퍼티가 초기화 되어야 한다
(기본값을 가지든, 생성자를 통해 초기화 하든!!!)
따라서 우리가 human이란 인스턴스를 만들고 init을 호출한 순간,
Human 인스턴스 내에 있는 모든 프로퍼티들이 초기화 되며, contacts란 프로퍼티도 초기화 된다!
근데 만약 lazy를 사용한다면?
class Contacts {
var email: String = ""
var address: String = ""
init() { print("Contacts Init") }
}
class Human {
var name: String = "unknown"
lazy var contacts: Contacts = .init()
}
let human: Human = Human()
print("Contacts Init")가 실행되지 않는다!
선언만 됐을 뿐 contacts란 변수 자체가 초기화 되지 않는 것임
이제, 다음과 같이 내가 contacts란 변수에 처음으로 접근하고자 하면
human.contacts.address = "none"
이때 print("Contacts Init")가 실행된다!
클래스, 구조체, 열거형에서 사용된다
저장 프로퍼티와 달리 저장 공간을 갖지 않고, 다른 "저장 프로퍼티"의 값을 읽어 연산을 실행하거나, 프로퍼티로 전달받은 값을 다른 프로퍼티에 저장한다
따라서 항상 var로 선언되어야 한다!!
var name: Type {
get { //getter (다른 저장 연산프로퍼티의 값을 얻거나 연산하여 리턴할 때 사용)
statements
return expr
}
set(name) { //setter (다른 저장프로퍼티에 값을 저장할 때 사용)
statements
}
}
// 잘못된 코드, 연산 프로퍼티 내부에서는 다른 저장 프로퍼티를 가져와서 사용해야 한다.
class Person {
var alias: String {
get {
return alias
}
set(name) {
self.alias = name
}
}
}
class Person {
var name: String = "human"
var alias: String {
get {
return name
}
set(name) {
self.name = name
}
}
}
name과 같이 읽거나 쓸 수 있는 "저장 프로퍼티"가 먼저 존재해야 하고,
연산 프로퍼티에선 이 다른 저장 프로퍼티
의 값을 읽거나 쓰는 작업을 해야 한다.
그렇다면 사용하는 방법은?
let human: Person = .init()
// get에 접근
print(human.alias) // "human"
// set에 접근
human.alias = "rename"
print(human.name) // "rename"
클래스, 구조체, 열거형에서 사용된다
저장 타입 프로퍼티와 연산 타입 프로퍼티가 존재하며,
저장 타입 프로퍼티의 경우 선언할 당시 원하는 값으로 항상 초기화가 되어 있어야 한다
static
을 이용해 선언하며, 자동으로 lazy
로 작동한다(lazy를 직접 붙일 필요 또한 없다)
쉽게 말하면, 저장&연산 프로퍼티 앞에 "static"이란 키워드만 붙이면
그것은 저장 타입 프로퍼티 & 연산 타입 프로퍼티가 되는 것이다.
class Human {
let name: String = "human" // 저장 프로퍼티
var alias: String { // 연산 프로퍼티
return name + "은 바보"
}
}
class Human {
static let name: String = "human" // 저장 타입 프로퍼티
static var alias: String { // 연산 타입 프로퍼티
return name + "은 바보"
}
}
저장 타입 프로퍼티의 경우, 항상 초기값을 가져야 한다
타입 프로퍼티는 인스턴스가 생성될 때마다 "매번 생성"되는 "기존 프로퍼티"와 다르다.
인스턴스가 생성 된다고 매번 해당 인스턴스의 멤버로 매번 생성되는 것이 아니라,
언제 한번 누군가 한번 불러서 메모리에 올라가면, 그 뒤로는 생성되지 않으며
언제 어디서든 이 타입 프로퍼티에 접근할 수 있는 것이다.
전역 변수로 생각하면 조금 더 편할 것 같다.
"누군가 나를 불러줬을 때 메모리에 올라간다"
Human.name // 이런 식으로 접근해서 사용한다.
타입 프로퍼티의 경우 기존 속성이 lazy이기 때문에
name이란 프로퍼티를 최초 호출하기 전까진, 초기화되지 않는다.
근데 name이란 타입 프로퍼티를 최초 호출하면,
이때 메모리에 올라가서 초기화 되는 것이다.
보통 모든 타입이 공통적인 값을 정의하는 데 유용하게 쓰인다. 싱글톤과 같은 맥락이라고 생각하면 된다.
함수
class Car {
func doSomething() {
print("car")
}
}
class Car {
func doSomething() {
print("car")
}
}
let car: Car = Car
car.doSomething()
Type Property와 유사하게 동작하낟.
형식(Type)에 연관된 메서드라서, 인스턴스와는 직접 연관되지 않은 사이인 것이다.
func이란 키워드 앞에 static 혹은 class가 붙은 메서드를 타입 메서드라고 하는데,
class Human {
static func sayHello() {
print("Hello")
}
class func sayBye() {
print("Bye")
}
}
Car.sayHello()
Car.sayBye()
타입 메서드를 선언하기 위해선 func 앞에 static 혹은 class를 붙여주면 된다고 했다.
그럼 언제 static을 쓰고, 언제 class를 쓸까?
구분 짓는 것은 메서드 오버라이딩(override)의 여부이다.
class Human {
static func sayHello() {
print("Hello")
}
}
class Person: Human {
override static func sayHello() { //Cannot override static method
}
}
이렇게 static으로 선언된 타입 메서드 sayHello같은 경우,
Subclass에서 override 하려고 하면 static method는 오버라이딩 에러가 난다.
class Sodeul {
class func sayBye() {
print("Bye")
}
}
class SodeulSodeul: Sodeul {
override class func sayBye() {
}
}
class로 선언되면 오버라이딩이 가능하다.
class Human {
let name = "human"
static let alias = "aa"
// type method
static func sayHello() {
print(name) // error! Instance member 'name' cannot be used on type 'Human
print(alias) // ok
}
}
class Human {
let name = "human"
static let alias = "aa"
//instance method
func sayHello() {
print(name)
print(Human.alias) // ok
}
}
델리게이트 패턴은 디자인 패턴 중 하나로, 프로그램 안에서 어떤 객체를 대신하여 행동한다던가, 다른 객체와 협동하여 일할 수 있게끔 권한을 위임하는 패턴이다.
Delegate의 사전적 정의: 위임(자)
Protocol을 이용해서 권한을 위임하고 일을 처리하는 방식의 디자인 패턴이다.
iOS에서 자주 사용하는 delegator인 UITableViewDelegate나 UICollectionViewDelegate의 타입은 프로토콜이다.
즉 위임받는 대리자(주로 사용자가 만든 컨트롤러 객체)가, 위임 받으려는 프로토콜을 채택하는 방식으로 위임의 과정이 이루어진다
delegate는 아르바이트생(대리자)이라고 생각하면 좋을 것 같다!!
붕어빵 가게에서 사장님이 바쁘니 아르바이트생이 업무를 대신하는 것!
크게 두가지 역할이 존재한다.
위임자와 대리자
위임자는 일반적으로 framework object이다.
대리자에 대한 참조를 유지한다.
적절한 시점에 대리자에게 메시지를 보낸다. (* 메시지 - 위임자가 다뤄왔거나, 방금 처리한 이벤트에 대한 알림)
대부분의 Cocoa framework 클래스의 위임자는 대리자에 의해 발생되는 알림의 관찰자로 자동으로 등록된다.
경우에 따라 위임자는 이벤트 처리 방법에 영향을 주는 값을 리턴할 수 있다.
일반적으로 사용자가 만든 컨트롤러 객체이다.
대리자는 특정 알림 메시지를 받기 위해 프레임워크 클래스에서 선언한 (채택한 프로토콜의 메서드만) 구현하면 된다.
대리자는 어플리케이션 안에 있는 다른 객체나 자신의 모양이나 상태를 업데이트 해 메시지에 응답한다.
여기서 기억해야 할 사항은 위임자와 대리자가 서로 메시지를 보내고 응답하면서 통신을 한다는 것이다.
protocol 이름 {
// property 가능
// method 가능
}
protocol BossDelegate {
func manageTasks()
}
// 대리자
struct Secretary: BossDelegate{
func manageTasks() {
print("오늘 할일이 산더미에요. 아자!")
}
}
// 대리자
struct Assistance: BossDelegate{
func manageTasks() {
print("쉬엄쉬엄 하셔도 됩니다 하핫")
}
}
// 위임자
struct Boss {
var delegate: BossDelegate //#1
}
var boss = Boss(delegate: Secretary()) //#2
boss.delegate = Assistance() //#3
boss.delegate.manageTasks()
싱글톤이란 ?
특정 용도로 객체를 하나만 생성하여,
공용으로 사용하고 싶을 때 사용하는 디자인 유형이다
이 클래스에 대한 Instance는 최초 생성될 때 딱 한번만 생성해서 전역에 두고,
그 이후로는 이 Instance만 접근 가능하게 하자
class UserInfo {
static let shared = UserInfo()
var id: String?
var password: String?
var name: String?
}
class UserInfo {
static let shared = UserInfo()
var id: String?
var password: String?
var name: String?
private init() { }
}
혹시라도 Init 함수를 호출해 Instance를 또 생생하는 것을 막기 위해,
init() 함수 접근 제어자를 private로 지정해주면 된다.
let userInfo = UserInfo.shared
userInfo.password = "123"
let screen = UIScreen.main
let userDefault = UserDefaults.standard
let application = UIApplication.shared
let fileManager = FileManager.default
let notification = NotificationCenter.default
예를 들어 모델 레이어에서 값이 변경되면, 감시하고 있던 뷰 레이어에게 전달되어 변경에 대응할 수 있는 것이다.
class MyObjectToObserve: NSObject {
@objc dynamic var myDate = NSDate(timeIntervalSince1970: 0) // 1970
func updateDate() {
myDate = myDate.addingTimeInterval(Double(2 << 30)) // Adds about 68 years.
}
}
감시할 수 있는 클래스에는 조건이 하나 있는데, 바로 NSObject 를 상속한 클래스여야 한다는 것이다.
그리고 KVO를 수행하려면 감시할 속성에 @objc 어트리뷰트와 dynamic 모디파이어를 붙여주어야 한다.
위 코드에서는 myDate 라고 하는 변수가 감시를 할 수 있는 속성이 된다.
class MyObserver: NSObject {
@objc var objectToObserve: MyObjectToObserve
var observation: NSKeyValueObservation?
init(object: MyObjectToObserve) {
objectToObserve = object
super.init()
observation = observe(
\.objectToObserve.myDate,
options: [.old, .new]
) { object, change in
print("myDate changed from: \(change.oldValue!), updated to: \(change.newValue!)")
}
}
}
다른 오브젝트에게 값의 변경을 알려주는 방법 중 하나인 KVO!
// 1) Delegate 프로토콜 선언
protocol SomeDelegate {
func someFunction(someProperty: Int)
}
class SomeView: UIView {
// 2) 순환 참조를 막기 위해 weak으로 delegate 프로퍼티를 가지고 있음
weak var delegate: SomeDelegate?
func someTapped(num: Int) {
// 3) 이벤트가 일어날 시 delegate가 동작하게끔 함
delegate?.someFunction(someProperty: num)
}
}
// 4) Delegate 프로토콜을 따르도록 함
class SomeController: SomeDelegate {
var view: SomeView?
init() {
view = SomeView()
// 6) delegate를 자신으로 설정
view?.delegate = self
someFunction(someProperty: 0)
}
// 5) Delegate 프로토콜에 적힌 메소드 구현
func someFunction(someProperty: Int) {
print(someProperty)
}
}
let someController = SomeController()
// prints 0
delegate는 커뮤니케이션 과정을 유지하고 모니터링하는 제 3의 객체가 필요가 없어 좋지만 많은 줄의 코드가 필요하며, 많은 객체들에게 이벤트를 알려주는 것이 어렵고 비효율적이다.
// 1) Notification을 보내는 ViewController
class PostViewController: UIViewController {
@IBOutlet var sendNotificationButton: UIButton!
@IBAction func sendNotificationTapped(_ sender: UIButton) {
guard let backgroundColor = view.backgroundColor else { return }
// Notification에 object와 dictionary 형태의 userInfo를 같이 실어서 보낸다.
NotificationCenter.default.post(name: Notification.Name("notification"), object: sendNotificationButton, userInfo: ["backgroundColor": backgroundColor])
}
}
// 2) Notification을 받는 ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 옵저버를 추가해 구독이 가능하게끔 함
NotificationCenter.default.addObserver(self, selector: #selector(notificationReceived(notification:)), name: Notification.Name("notification"), object: nil)
}
// iOS 9 이상이 아닐 경우에는 removeObserver를 해줘야 함
deinit {
NotificationCetner.default.removeObserver(self)
}
@objc func notificationReceived(notification: Notification) {
// Notification에 담겨진 object와 userInfo를 얻어 처리 가능
guard let notificationObject = notification.object as? UIButton else { return }
print(notificationObject.titleLabel?.text ?? "Text is Empty")
guard let notificationUserInfo = notification.userInfo as? [String: UIColor],
let postViewBackgroundColor = notificationUserInfo["backgroundColor"] else { return }
print(postViewBackgroundColor)
}
}
서로 데이터를 보내주고 통신 할 수 있도록 하는 걸
Notification Center라는 싱글턴 객체를 통해 이벤트들의 발생 여부를 옵저버를 등록한 객체들에게 Notification을 post하는 방식으로 사용한다.
이때 Notification name이라는 key 값을 통해 주고받을 수 있다.
notification은 많은 줄의 코드가 필요없이 쉽게 구현이 가능하며, 다수의 객체들에게 동시에 이벤트의 발생을 알려줄 수 있다. 하지만 notification post이후의 정보를 받을 수 없다는 단점이 있다.
한 객체에서 이벤트가 발생했다는 것을 Notification Center로 송신
Notification Center에서 발생된 이벤트를 등록된 모든 옵저버에 보내고 해당 이벤트를 구독하는 Observer가 있다면 해당 화면에서 이벤트에 대한 처리
만약 등록된 옵저버가 많아지면 모든 옵저버리스트를 찾아가기에 성능이 저하될 수 있.