- 파운데이션(Foundation) 프레임워크란? MacOS, iOS 애플리케이션 개발의 기초적인 기능을 제공하는 프레임워크로 클래스 이름 앞에 NS~로 시작되는 클래스로 가장 최상위 부모 클래스는 NSObject 클래스이다.
- 파운데이션 프레임워크에 있는 주요 기능
- 데이터 다루기
- 네트워크
- 파일 다루기
- 멀티 쓰레드
- Swift와 Foundation 프레임워크 : Swift2까지는 Objective-C 기반의 클래스와 API를 유지하여 NSString, NSMutableString, NSArray, NSMutableArray 등으로 제공했지만, Swift3부터 Swift 용으로 포팅하여 API Guideline을 적용하여 NS를 붙이지 않아도 사용 가능해졌다. NSString -> String 으로도 사용 가능하다.
- 파운데이션 프레임워크에서 가장 기본 클래스 : NSObject
- 제공되는 기능
- 객체 비교
- 메모리 관리
- 타입 체크
- 셀렉터
- NSObject 클래스를 상속하여 사용할지 아니면 그냥 Swift에서 제공하는 클래스를 사용할지는 셀렉터를 사용하면 무조건 NSObject 클래스를 상속받아 사용하고 그렇지 않은 경우는 NSObject 클래스를 상속하지 않아도 무관하다.
- 셀렉터 : 셀렉터는 메소드를 식별하는 정보로 사용되며, 주로 클래스에 정의된 메소드를 검사한 후에 실행하는데 사용된다.
- 사용방법셀렉터 사용방법
// 셀렉터 정의
#selector(TYPE.Method_Name)
// 셀렉터 검사
func responds(to aSelector : Selector!) -> Bool
// 셀렉터 실행
// 파라미터가 없는 메소드 실행
func perform(_ aSelector : Selector!) -> Unmanaged<AnyObject>!
// 파라미터가 있는 메소드 실행
func perform(_ aSelector : Selector!, with object:Any!) -> Unmanaged<AnyObject>! - 파라미터가 없는 메소드에서 셀렉터 사용방법파라미터가 없는 메소드
// Class 정의
class MyClass : NSObject {
@objc func greeting() { print("Hello") }
}
let obj = MyClass()
// 셀렉터 정의
let sel = #selector(MyClass.greeting)
// 셀렉터 검사
if obj.responds(to : sel) {
// 셀렉터로 메소드 실행
obj.perform(sel)
} - 파라미터가 있는 메소드에서 셀렉터 사용방법파라미터가 있는 메소드
// Class 정의
class MyClass : NSObject {
@objc func sayHello(who : String) {
print("Hello \(who)")
}
}
let obj = MyClass()
// 셀렉터 정의
let sel = #selector(MyClass. sayHello(who : ))
// 셀렉터 검사
if obj.responds(to : sel) {
// 셀렉터로 메소드 실행
obj.perform(sel, with : "Selena")
} - 셀렉터로 사용되는 메소드 앞에 @objc 키워드는 Objective-C 메소드로 사용한다는 의미로 셀렉터로 사용하려는 메소드 앞에 반드시 붙여야 하고, 붙이지 않을 경우 에러가 발생한다.

- 파일 다루기
- FileManager : 파일 시스템을 다룬다.
- 사용 방법 : let filmanager = FileManager.default 로 선언하여 사용한다.
- 파일 존재확인파일 존재 확인 방법
// FileManager 선언
let fm = FileManager.default
let checkFilePath = "/User/Document/Test.txt"
// 파일 존재여부 확인
if(fm.fileExists(atPath : checkFilePath)) {
print("파일 존재")
} else {
print("파일 존재하지 않음")
} - 폴더 내 파일 목록 확인폴더 내 파일 목록 확인 방법
// FileManager 선언
let fm = FileManager.default
let path = "/User/Document"
// 폴더 존재여부 확인
if(fm.fileExists(atPath : path)) {
let fileList = try? fm.contentOfDirectory(atPath : path)
print("파일 목록 : \(fileList)")
} else {
print("파일 존재하지 않음")
} - 파일 복사파일 복사 방법
// FileManager 선언
let fm = FileManager.default
let srcPath = "/User/Document/Test.txt"
let desPath = "/User/Test/Test.txt"
//복사하려는 파일 존재여부 확인
if(fm.FileExists(atPath : srcPath) == false) {
print("복사할 파일이 없음")
} else if(fm.FileExists(atPath : desPath) == false) {
print("복사할 폴더에 이미 파일이 있음")
} else {
print("복사 중...")
do{
try fm.copyItem(atPath : srcPath, toPath : desPath)
} catch let err {
print("복사 실패 : \(err)")
}
} - 파일 이동파일 이동 방법
// FileManager 선언
let fm = FileManager.default
let srcPath = "/User/Document/Test.txt"
let desPath = "/User/Test/Test.txt"
// 이동하려는 파일 존재여부 확인
if(fm.FileExists(atPath : srcPath) == false) {
print("이동할 파일이 없음")
} else if(fm.FileExists(atPath : desPath) == false) {
print("이동할 폴더에 이미 파일이 있음")
} else {
print("이동 중...")
do{
try fm.moveItem(atPath : srcPath, toPath : desPath)
} catch let err {
print("이동 실패 : \(err)")
}
} - 파일 삭제파일 삭제 방법
// FileManager 선언
let fm = FileManager.default
let delPath = "/User/Document/Test.txt"
// 삭제하려는 파일 존재여부 확인
if(fm.FileExists(atPath : delPath) == false) {
print("삭제할 파일이 없음")
} else {
print("삭제 중...")
do{
try fm.removeItem(atPath : delPath)
} catch let err {
print("삭제 실패 : \(err)")
}
} - 직렬화
- 직렬화는 생성한 객체를 바이너리 데이터로 변경하는 것으로 객체를 바이너리 데이터로 변환하는 것을 아카이브(Archive) 즉, 직렬화이고, 바이너리 데이터를 다시 객체로 변환하는 것을 언아카이브(Unarchive) 즉, 역직렬화라고 한다.
- 직렬화는 네트워크 전송이나 파일에 내용을 쓸 때 주로 사용된다.
- 바이너리 데이터는 Data 객체로 변환된다.
- 데이터 직렬화 방법
- NSKeyedArchiver : 객체를 바이너리 데이터로 변환 (Any -> Data, File)
- NSKeyedUnarchiver : 바이너리 데이터를 객체로 변환 (Data, File -> Any)
- 사용방법객체 직렬화
let num = 2020
// 직렬화
let data : Data = NSKeyedArchiver.archivedData(withRootObject : num)
// 역직렬화
if let num2 = NSKeyedUnarchiver.unarchiveObject(withData : data) as? Int {
print("Dat에서 복원 성공 : \(num2)")
} else {
print("복원 실패 ")
} - 다수의 데이터 직렬화 방법
- NSMutableData : 키 - 값 방식으로 직렬화를 진행한다.
- encode : 값, 키를 파라미터로 받는 메소드로 값을 직렬화 한다.
- decode : 키를 파라미터로 받는 메소드로 키에 해당하는 값을 역직렬화 한다.
- 사용방법다수의 데이터 직렬화
let mdata = NSMutableData()
// 인코딩
let archiver = NSKeyedArchiver(forWritingWith : mdata)
archiver.encode(true, forKey : "BoolData")
archiver.encode(177, forKey : "IntData")
archiver.encode("Hello", forKey : "StrData")
archiver.finishEncoding()
let data = archiver.encodedData
// 디코딩
let unarchiver = try?NSKeyedUnarchiver(forReadingFrom : data)
let boolData = unarchiver.decodeBool(forKey : "BoolData") // true 저장
let intData = unarchiver.decodeInteger(forKey : "IntData") // 177 저장
let strData = unarchiver.decodeObject(forKey : "StrData")
// Hello 저장 - 커스텀 타입의 직렬화 방법
- NSCoding 프로토콜 : 인코딩 / 디코딩 함수를 정의하며 클래스만 가능하고 구조체는 불가능하다.
- 사용되는 메소드
- 인코딩 메소드 : func encode(withCoder aCoder : NSCoder)
- 디코딩 메소드 : init?(coder aDecoder : NSCoder)
- 사용방법커스텀 타입의 직렬화
// 클래스 정의
class Person : NSObject, NSCoding {
var name : String
var birthYear : Int
// 인코딩 메소드
func encode(with aCoder : NSCoder) {
aCoder.encode(name, forKey : "Name")
aCoder.encode(birthYear, forKey : "Birth")
}
// 디코딩 메소드
required init(coder aDecoder : NSCoder) {
name = aDecoder.decodeObject(forKey : "Name") as! String
birthYear = aDecoder.decodeInteger(forKey : "Birth")
}
// initializer 로 정보를 받아옴
init(name : String, birthYear : Int) {
self.name = name
self.birthYear = birthYear
}
}
// 인코딩 / 디코딩을 사용한 객체 생성
// 객체 생성
var person = Person(name : "박문수", birthYear = 1355)
// 인코딩
let data = NSKeyedArchiver.archivedData(withRootObject : person)
// 디코딩
let obj = NSKeyedUnarchiver.unarchiverObject(with : data) as! Person
print("name : \(obj.name), birthYear ; \(obj.birthYear)")
// name : 박문수, birthYear : 1355 출력 - 타이머
- 타이머란? 수행할 동작을 특정 시간 이후에 실행하도록 하는 기능이다.
- Timer 클래스를 이용하여 타이머를 시작하고, 중지할 수 있다.
- 사용방법타이머 사용
// 클래스 정의
class Alarm : NSObject {
// 특정 시간 이후에 실행할 동작을 정의한 메소드
@objc func ring(timer : Timer) {
print("Wake Up!!")
}
}
// 객체 생성
let alarm = Alarm()
// 타이머 생성
var timer = Timer.scheduledTimer(timeInterval : 1, target : alarm, selector:#selector(Alarm.ring), useInfo : nil, repeats : false)
// 타이머 바로 시작
timer.fire() // Wake Up!! 출력
// 클로저를 이용한 Timer
Timer.scheduledTimer(withTimerInterval : 1.0, repeats:false) {
timer in print("클로저를 이용한 타이머")
} - 알림
- 알림이란? 특정 조건에 수행할 동작을 옵저버를 이용하여 동작하는 기능이다.
- 알림 구성
- 알림(Notification)
- 알림 센터(NotificationCenter) : 알림을 발송하고, 알림 감시자(Observer)를 등록한다.
- 알림 감시자(Observer) : 알림 발생 시 동작을 정의하고, 더 이상 필요하지 않다면 제거한다.
- 사용 방법알림 사용
// 클래스 정의
class MyClass : NSObject {
// 특정 시간 이후에 실행할 동작을 정의한 메소드
@objc func handleNoti(noti : Notification) {
print("Hello Swift Custom Alarm")
}
}
// 알림 센터 선언
let notiCenter = NotificationCenter.default
// 알림 이름 정의
let customNoti = Notification.Name("CustomNotification")
// 옵저버 추가
let myClass = MyClass()
notiCenter.addObserver(myClass, selector : #selector(MyClass. handleNoti), name: customNoti, object: nil)
// 알림 발생
notiCenter.post(name: customNoti, object: nil)
// Hello Swift Custom Alarm 출력 - 멀티 쓰레드
- 쓰레드란? 오래 걸리는 작업을 사용자가 기다리지 않고 수행할 수 있도록 처리하는 기능이다.
- 쓰레드 사용방법
- 셀렉터를 이용한 쓰레드셀렉터를 이용한 쓰레드
// 클래스 정의
class MyClass : NSObject {
// 0부터 10까지 출력하는 메소드
@objc func countTen() {
for count in 0...10 {
print(count)
}
}
}
// 객체 선언
let obj = MyClass()
// 셀렉터를 이용한 쓰레드 - 1
obj.performSelector(inBackground : #selector(MyClass.countTen), with: nil)
// 셀렉터를 이용한 쓰레드 - 2
Thread.detachNewThreadSelector(#selector(MyClass.countTen), toTarget: obj, with: nil)
sleep(10)
// 슬립을 주지 않으면 쓰레드가 실행되기 전에 프로그램이 종료되어 쓰레드가 동작하는 것을 볼 수 없음 - 클로저를 이용한 쓰레드클로저를 이용한 쓰레드
// 클로저를 이용한 쓰레드
Thread.detacthNewThread {
for count in 0...10 {
print(count)
}
}
sleep(10)
// 슬립을 주지 않으면 쓰레드가 실행되기 전에 프로그램이 종료되어 쓰레드가 동작하는 것을 볼 수 없음 - Thread의 자식 클래스를 작성한 쓰레드Thread의 자식 클래스를 사용
// 쓰레드 정의
class MyThread : Thread {
// 쓰레드가 수행할 동작 정의
override func main() {
for count in 0...10 {
print(count)
}
}
}
let thread = MyThread()
thread.start() // 쓰레드 동작 시작
sleep(10)
// 슬립을 주지 않으면 쓰레드가 실행되기 전에 프로그램이 종료되어 쓰레드가 동작하는 것을 볼 수 없음 - 멀티 쓰레드는 이런 쓰레드가 여러개 동작하는 것을 의미한다.
- 예시는 아래와 같다.멀티 쓰레드 - 쓰레드가 여러개 동작
// 쓰레드 정의
class MyThread : Thread {
// 쓰레드가 수행할 동작 정의
override func main() {
for count in 0...10 {
print(count)
}
}
}
let thread1 = MyThread()
thread1.start() // 쓰레드 동작 시작 - 1개
let thread2 = MyThread()
thread2.start() // 쓰레드 동작 시작 - 2개
// 클로저를 이용한 쓰레드
Thread.detacthNewThread {
for count in 0...10 {
print(count)
}
} // 쓰레드 동작 시작 - 3개
sleep(10)
// 슬립을 주지 않으면 쓰레드가 실행되기 전에 프로그램이 종료되어 쓰레드가 동작하는 것을 볼 수 없음 - 멀티 쓰레드를 조절하는 멀티 쓰레드 클래스 Operation, OperationQueue
- Operation은 큐를 기반으로 동작한다.
- 동시 동작하는 쓰레드 개수를 조절 : var maxConcurrentOperationCount : int
- 쓰레드 동작/취소
- 쓰레드 동작 : func addOperation(_ op : Operation) 함수에 추가
- 쓰레드 취소 : func cancelAllOperations()
- 큐를 이용한 쓰레드 관리 : OperationQueue 클래스
- 사용방법Operation을 이용한 멀티 쓰레드 사용
// Operation 정의
class MyOperation : Operation {
// 수행할 동작 정의
override func main() {
for count in 0...10 {
print(count)
}
}
}
// OperationQueue 선언
let opQueue = OperationQueue()
// 최대 실행 개수 2개까지 허용
opQueue.maxConcurrentOperationCount = 2
let operation1 = MyOperation()
let operation2 = MyOperation()
let operation3 = MyOperation()
// 클로저를 이용한 Operation 실행
opQueue.addOperation({
for count in 0...10 {
print(count)
}
}) // 1개 실행
opQueue.addOperation(operation1) // 2개 실행
// 대기 : 한번에 최대 2개까지만 실행하게 설정
opQueue.addOperation(operation2)
// 위 2개 중에 하나의 실행이 끝날 때 까지 대기
opQueue.addOperation(operation3)
// 위 3개 중에 둘의 실행이 끝날 때까지 대기
sleep(10)
// 슬립을 주지 않으면 쓰레드가 실행되기 전에 프로그램이 종료되어 쓰레드가 동작하는 것을 볼 수 없음 - 네트워크
- 네트워크 사용 시 필요 정보는 URL(리소스 위치 정보), 요청(URLRequest), 응답(URLResponse) 이다.
- URL 정보
- 네트워크를 통해 접속할 위치 정보를 의미한다.
- URL 허용 문자는 알파벳, 숫자, 하이픈(/), 닷(.) 등으로 제한된 문자만 허용한다.
- 그 이외 문자는 퍼센트 인코딩 필요
예) https://www.google.com/search?q=아이폰
-> https://www.google.com/search?q=%EC%95%84%EC%9D%B4%ED%8F%B0URL을 위한 퍼센트 인코딩/디코딩let urlStr = "http://www.google.com?q=아이폰"
// 인코딩
let encoded = urlStr.addingPercentEncoding(withAllowedCharacters : .urlQueryAllowed)!
/* 출력 : http://www.google.com?q=%EC%95%84%EC%9D%B4%ED%8F%B0 */
// 디코딩
let decoded = encoded.removingPercentEncoding!
/* 출력 : http://www.google.com?q=아이폰
*/ - URL 분석
- URL 주소는 Scheme, host, path, query 로 구성된다.URL 구성 요소
let url = URL(string: "http://google.com?q=iPhone&format=json")!
// scheme
url.scheme // http
// host
url.host // google.com
// path
url.path //
// query
url.query // q=iPhone&format=json - 요청(URLRequest)과 응답(URLResponse)
- 사용 예시를 통해서 URL 주소에 요청하고 요청한 내용을 응답받는 예제는 아래와 같다.URL 요청과 응답 처리
// 요청할 URL
let url = URL(string: "http://google.com?q=iPhone&format=json")!
// 요청 : URLRequest
let request = URLRequest(url : url)
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: request) {
// 클로저를 이용한 응답
(data : Data?, response : URLResponse?, error : Error?) in
let httpResponse = response as! HTTPURLResponse
print("status code : ", httpResponse.statusCode)
let headers = httpResponse.allHeaderFields
for(key, value) in headers {
print("\(key) : \(value)")
}
}
task.resume()
Swift 파운데이션 프레임워크
Swift 접근 조절
- 접근 조절 : 선언한 변수, 프로퍼티, 메소드, 클래스가 어느 수준까지 접근 가능한지 정의하는 것으로 Swift에서는 5단계가 있다.
- 접근 조절 레벨
- open : 다른 모듈에서 접근과 상속이 모두 가능하다.
- public : 다른 모듈에서 접근 가능하지만 상속은 불가능하며, 상속은 동일 모듈에서만 가능하다.
- internal : 같은 모듈 내에서만 접근 및 상속이 가능하며, 기본 값이다.
- fileprivate : 같은 소스 파일 내에서만 접근 가능하다.
- private : 정의된 블록 내에서만 가능하다.
- 접근 조절 레벨에서 open, public은 프레임워크를 만들 경우 주로 사용된다.
- 접근 조절 단위 : 접근 조절 레벨을 적용할 수 있는 단위이며, 타입, 메소드, 프로퍼티가 있다.
- 접근 조절 단위는 프로퍼티, 메소드, 타입 순으로 포함이되며, 타입이 private이면, 메소드, 프로퍼티는 모두 private만 가능하다.
Swift 제네릭스
- 제네릭스 : 타입에 대한 추상화를 시키는 프로그램 기법으로 타입별로 작성할 필요없이 어떤 타입이든 적용가능하다.
- 사용방법
- 배열에서의 사용 : 배열 정의 시에 Element 키워드를 이용하여 타입 추상화를 시킴배열에서의 제네릭스
// 제네릭스 프로그램 기법 이용한 배열 선언
struct Array<Element> {
func append(_ newElement : Element) -> Void
func remove(at index : Int) -> Element
}
// 선언한 배열 사용
// String 형 배열
var strArray = Array<String>()
strArray.append("Hello")
strArray.append("Swift")
strArray.remove(at index : 0)
// Int 형 배열
var intArray = Array<Int>()
intArray.append(10)
intArray.append(11.1) // 에러 발생
/* Swift는 강한 타입 체크로 Int형 값을 넣으라는 에러 발생 */ - Dictionary에서의 사용(두 종류 타입 추상화 가능 )Dictionary에서의 제네릭스
// 제네릭스 프로그램 기법 이용한 Dictionary 선언
struct Dictionary<Key : Hashable, Value> {
func updateDicValue(_ value : Value, forKey : Key) -> Value?
func remoteDicValue(forKey : Key) -> Value?
}
// 선언한 Dictionary 사용
// Key가 Int, Value가 String인 Dictionary
var intAndstrDic = Dictionary<Int, String>()
intAndstrDic.updateDicValue("Hello", 0)
var strAndstrArrayDic = Dictionary<String, [String]>()
strAndstrArrayDic.updateDicValue("1월", ["Jan","1M"]) - 함수에서의 사용함수에서의 제네릭스
// 출력함수에 어떤 타입이든 출력가능하게 제네릭스로 선언
func printValue<T> (_ value : T) {
print("데이터 출력 : \(value)")
}
// 타입에 상관없이 출력 가능
printValue("Hello") // 데이터 출력 : Hello 출력
printValue(3) // 데이터 출력 : 3 출력 - 클래스에서의 사용클래스에서의 제네릭스
// 타입 추상화를 이용하여 클래스 선언
class Hospital<T> {
func inHospital(patient : T) {
print("병원에 입원")
}
}
// Human
class Human {}
// Pet
class Pet {}
// Hospital 클래스 선언 : Human 클래스를 이용
var hospital = Hospital<Human>()
// Hospital 클래스 선언 : Pet 클래스를 이용
var petHospital = Hospital<Pet>()
Swift Any와 타입체크
- Swift 에서의 타입 체크는 강한 타입체크로 변수/상수 선언 시에 정해진 타입을 확정하면 다른 타입의 데이터는 대입할 수 없다. (선언한 타입으로 대입하라고 에러 발생)
- Any를 이용하여 타입에 자유로운 가변 타입으로 선언하여 타입을 확정하지 않고, 사용 시에 필요한 타입을 사용할 수 있다.
- 사용 방법
- 위 코드를 보면 타입을 확정하여 선언 시와 다르게 다른 타입을 대입해도 에러가 발생되지 않는다.
- 가변 타입 Any는 타입에 대한 정보가 없기 때문에 타입별로 사용하는 메소드, 프로퍼티는 사용할 수 없고, 타입 체크 후에 타입을 확정하는 과정이 필요하다.
- 타입 정보 얻는 방법 : type(of : )타입 정보 얻기
// Any를 이용하여 타입 선언
var anyValue : Any = 10
print(type(of : anyValue)) // Int를 출력 - 타입 체크하는 방법 : is Type명타입 체크
// Any를 이용하여 타입 선언
var anyValue : Any = 10
if anyValue is Int { // is를 이용한 타입 체크
print("정수 타입")
} else {
print("정수 이외의 타입")
}
// 상속과 타입 체크
class Animal {} // Parent 클래스
class Dog : Animal {} // Child 클래스
class Cat : Animal{} // Child 클래스
var bingo : Animal = Dog("빙고")
bingo is Dog // true
bingo is Animal // true
/* Child 클래스는 Parent 클래스와 is a 관계가 성립하여 타입체크 시 Parent 클래스로 확인해도 true가 반환 */
bingo is Cat // false - 타입 변환하는 방법 : as타입 변환
// Any를 이용하여 타입 선언
var anyValue : Any = 10
let intVal1 = anyValue as Int // 타입 변환이 확실한 경우
let intVal2 = anyValue as? Int // 타입 변환 불가 시 nil
let strVal = anyValue as! String
/*강제 타입 변환으로 변환 실패 시 컴파일 에러 발생*/
// 상속과 타입 변환
class Animal {} // Parent 클래스
class Dog : Animal {} // Child 클래스
class Cat : Animal{} // Child 클래스
var bingo = Dog()
var animal = bingo as Animal // 타입 변환 성공
/* Child 클래스에서 Parent 클래스로 타입 변환하는 업 캐스팅은 항상 성공한다. */
var obj : Animal = Cat()
let cat1 = obj as Cat // 컴파일
let cat2 = obj as? Cat // 변환 성공, 옵셔널 타입
// let cat2 : Cat? = Cat() 과 동일
let cat3 = obj as! Cat // 변환 성공, Non-옵셔널 타입
// let cat3 : Cat = Cat() 과 동일
/* Parent 클래스에서 Child 클래스로 타입 변환하는 다운 캐스팅은 as 인 경우를 제외하고 옵셔널이나 강제 변환은 가능하다. */
Swift 에러처리
- 프로그램에서 에러 발생 : 프로그램 실행 중에 예기치 못한 상황이 발생한 상태로 프로그램이 강제 종료되어 앱이 크래쉬되는 상황 또는 제대로 동작을 수행하지 못하는 상황이 발생한 상태를 의미한다.
예) 파일 처리 중 디스크 에러, 파일 처리 중 권한 부족으로 인한 에러 - 에러 발생 시 사용하는 API
- 메소드에 throws 키워드를 넣어 선언throws 로 선언
// 메소드 선언 시에 throws 키워드 사용하여 선언
func openFile(toFile : String) throws {
// 파일 열기에 대한 동작 정의
}
/* throws로 선언한 메소드는 반드시 try 키워드를 붙여서 사용*/
try str.openFile(toFile : sFilePath)
/* 위 상태에서 에러 발생해도 강제 종료가 되며 3번 에러 다루기에서 강제 종료가 발생되지 않도록 처리부분 정의*/ - Error 프로토콜을 채택한 객체의 에러 반환throw 를 이용한 에러 반환
/* Enum으로 Error 프로토콜을 채택하여 커스텀 에러 정의
자세한 것은 4번 커스텀에러에서 다룬다.
*/
enum CustomError : Error {
case myFault
case yourFault
}
// 정의된 에러 반환
func inputPositive(val : Int) throws {
guard (val > 0) else {
throw CustomError.yourFault // 에러 반환
// Error 프로토콜을 채택하면 반환
}
print("정상 수행")
} - 에러 다루기
- 에러가 발생하면 앱의 강제 종료를 방지하기 위해서 do-catch 문을 사용한다.
- do 내에 코드 실행 중 에러가 발생하게 되면 catch 내 코드를 실행하게 된다.do-catch 문 사용
// 메소드 선언 시에 throws 키워드 사용하여 선언
func openFile(toFile : String) throws {
// 파일 열기에 대한 동작 정의
}
do {
try str.openFile(toFile : sFilePath)
} catch {
print("파일 열기 중 에러 발생")
}
/* do 블록 내의 코드 실행 중에 에러 발생 시 catch 블록 내의 코드를 실행한다.*/ - do-catch문은 자바의 try-catch문과 유사하다.do-catch 문 사용 (Swift)try-catch 문 사용 (Java)
do {
// 에러 발생 가능 코드
try obj.throwsMethod
} catch {
// 에러 발생 처리 코드
}try {
// 에러 발생 가능 코드
} catch(exception e) {
// 에러 발생 처리 코드
} - 커스텀 에러
- 특정 에러를 별도로 정의해서 사용하는 것으로 Error 프로토콜을 채택하여 사용한다.
커스텀 에러 정의
// enum을 이용한 정의 ()
enum CustomError : Error {
case myFault
case yourFault
}
// enum으로 정의된 에러 다루기
do {
throw CustomError.yourFault
} catch CustomError.myFault {
print("내탓")
} catch CustomError.yourFault {
print("니탓")
}
// Switch-Case와 비슷하게 사용
// 구조체를 이용한 정의
struct CustomErrorStruct : Error {
var msg : String
}
// 클래스를 이용한 정의
class CustomErrorClass : Error {
}
// 구조체나 클래스는 where을 이용한 타입 체크로 구분
do {
// 에러 발생 시키기
let error = CustomErrorStruct(msg : "Oh! My God!!")
throw error // 에러 반환
} catch where error is CustomErrorStruct {
print("구조체로 작성한 에러 발생")
} catch where error is CustomErrorClass {
print("클래스로 작성한 에러 발생")
} - 클린업(defer)
- 에러가 발생여부와 상관없이 끝나기 전에 블록 내의 코드를 실행한다.
- 에러가 발생해도 실행되고, 정상 종료 시에도 실행된다.
- 자바의 try-catch-finally에서 finally와 유사한 기능이다.
- 사용 방법defer 사용법
// defer는 예외가 발생하는 곳에 작성
func dangerousFunc() throws {
defer{
print("메소드 실행 후 마지막 동작 정의")
}
// throw 보다 이전에 작성한다.
throw CustomErrorStruct(msg : "에러 발생")
}
Swift 프로토콜과 extension
- 프로토콜이란? 오직 인터페이스만 있는 상태로 구현하는 부분없이 프로퍼티나 메소드를 선언한 형태로 정의된 API를 의미한다.
- 프로토콜 정의와 채택(사용)
- 프로토콜 정의프로토콜 정의
protocol PROTOCOL_NAME {
// 프로퍼티 선언
// 메소드 선언
} - 프로토콜 채택 : 클래스나 구조체에서 정의된 프로토콜을 이용하는 것을 프로토콜을 채택했다고 한다. 프로토콜 채택 시에는 프로토콜에 정의된 프로퍼티, 메소드는 반드시 클래스나 구조체에서 구현이 되어야 한다.프로토콜 채택
// 프로토콜 정의
protocol PROTOCOL_NAME {
// 프로퍼티 선언
// 메소드 선언
}
// 클래스에서 프로토콜 채택
class CLASS_NAME : PROTOCOL_NAME {
}
// 구조체에서 프로토콜 채택
struct STRUCT_NAME : PROTOCOL_NAME {
} - 클래스의 상속과 프로토콜 : 클래스에서는 부모 클래스를 상속하고, 프로토콜 채택을 모두 할 수 있으며 아래와 같이 정의한다.클래스 상속과 프로토콜
// 프로토콜 정의
protocol PROTOCOL_NAME {
// 프로퍼티 선언
// 메소드 선언
}
// 부모 클래스
class PARENT_CLASS {
}
// 자식 클래스에서 부모 클래스 상속과 프로토콜 채택
class CHILD_CLASS : PARENT_CLASS, PROTOCOL_NAME {
}
/* 부모 클래스를 먼저 쓰고, 그 다음에 콤마 다음에 프로토콜을 쓴다.*/ - 프로토콜 상속 : 프로토콜은 다중 상속이 가능하여 여러 프로토콜을 상속할 수 있다.프로토콜 정의
protocol Singing {
func sing() {}
}
protocol Dancing {
func dance() {}
}
// Singing, Dancing에 정의된 메소드를 모두 구현해야한다.
protocol Entertaining : Singing, Dancing {
}
// Entertaining 프로토콜 채택 시 상속된 모든 메소드를 구현
class Human : Entertaining {
func sing() {print("라라라")}
func dance() {print("둠칫둠칫")}
} - 프로토콜 내 프로퍼티 선언 : 프로퍼티의 타입 선언 가능하고, 구현 시에 저장 프로퍼티로 사용할지 계산 프로퍼티로 사용할지 프로토콜 채택 후에 정한다.프로퍼티 선언
protocol Singing {
// 프로퍼티 선언
var duration : Int {get set}
}
// 저장 프로퍼티로 구현
struct MyStruct : Singing {
var duration : Int
}
// 계산 프로퍼티로 구현
class MyClass : Singing {
var duration : Int {
get { return 0 }
set { }
}
} - 프로토콜 내 Initializer 선언 : 프로토콜 내에 Initializer 선언이 가능하고, 클래스에서 구현 시에는 required를 추가한다.프로토콜 내 Initializer 구현
protocol Singing {
var sing : String
// Initializer
init(sing : String)
}
// 클래스가 프로토콜을 채택 시 Initializer 구현
class Monster : Singing {
required init(sing : String) {
self.sing = sing
}
}
// 구조체가 프로토콜을 채택 시 Initializer 구현
struct Boss : Singing {
init(sing : String) {}
} - 프로토콜을 타입으로 사용 시 채택한 클래스나 구조체에 별도의 메소드가 구현되어 있어도, 프로토콜 내의 메소드, 프로퍼티만 사용 가능하다.프로토콜 내 Initializer 구현
protocol Singing {
// 메소드
func sing() {}
}
// 클래스가 채택하여 구현
class Human : Singing {
func sing() {
print("랄랄랄")
}
func dancing() {
print("쿵쿵쿵")
}
}
// 프로토콜을 타입으로 사용
var singingAnimal : Singing = Human()
singingAnimal.sing()
singingAnimal. dancing()// 사용 못함 - extension(확장)
- 기존에 작성된 타입(클래스나 구조체, enum)을 확장해서 사용할 수 있다.
- extension을 할 경우 하나의 타입으로 동작한다.extension 예시
class Dog{
func eat() {
print("뭐든 잘 먹어요.")
}
}
// 확장
extension Dog {
func sing() {
print("멍~멍~")
}
}
var myDog = Dog()
myDog.eat()
myDog.sing() // 확장한 메소드 사용 가능 - extension이 가능한 것과 불가능한 것
- 가능한 것
- convenience initializer
- 계산 프로퍼티
- 메소드
- 프로토콜
- 서브스크립트
- nested type
- 불가능한 것
- designated initializer
- 저장 프로퍼티
- 프로토콜과 프로토콜 extension
- 기존에 정의된 프로토콜을 확장하여 사용 가능하며, 프로토콜의 다중 상속을 이용하여 코드 중복을 없애는데 사용된다.
- 코드 중복이 발생되는 상황코드 중복 발생
class Bird{
func move() {
print("날아서 이동")
}
func sound() {
print("소리를 낸다.")
}
}
class Airplane{
func move() {
print("날아서 이동")
}
func sound() {
print("소리를 낸다.")
}
} - 코드 중복을 없애는데 프로토콜 확장을 사용프로토콜 확장으로 코드 중복 제거
protocol Movable() { }
extension Movable {
func move() {print("날아서 이동")}
}
protocol Soundable {}
extension Soundable {
func sound() {print("소리를 낸다.")}
}
// 중복되는 메소드를 프로토콜 확장으로 제거
class Bird : Movable, Soundable {
}
class Airplane : Movable, Soundable {
}
Swift 클로저
- 클로저란? 함수를 객체처럼 사용하며, 함수처럼 정의한 후에 사용 시에 부가 기능을 더 추가할 수 있도록 한 API를 의미하며 형태는 함수나 코드 블록으로 구성된다.
- 클로저 사용 API
- 클로저의 함수 타입을 결정한다.
- 함수 타입 : 함수 객체를 참조 변수로 사용할 때의 타입을 의미한다.
- 사용방법
- 파라미터가 없고, 반환값이 없는 함수 타입 : () -> (), () -> Void
- 파라미터가 있고, 반환값이 있는 함수 타입 : (Int) -> Int
- 클로저 표현식
- 별도의 함수 정의 없이 Inline 방식으로 작성한다.클로저 표현식
// 방식
{ (파라미터, ...) -> Return Type in
// 코드
}
// 예시
array.sorted(by : {(a : Int, b : Int) -> Bool in
return a < b
}) - 클로저 표현식 단축
- 파라미터 타입 선언 생략
array.sorted(by : {a, b -> Bool in return a < b}) - 1줄 return인 경우 return 생략
array.sorted(by:{a, b -> Bool in a<b}) - 반환 타입 선언 생략
array.sorted(by:{a, b in a<b}) - 파라미터 선언 생략
array.sorted(by : {$0 < $1}) - 반환 값으로 사용한다.반환 값으로 사용
// 반환 값의 함수 타입 : () -> Void
func greeting() -> (() -> Void) {
// 함수 타입이 () -> Void 로 함수로 구성
func sayGoodmorning() {
print("Good Morning Everyone")
}
return sayGoodmorning
}
// 사용하기
let ret = greeting()
ret() // Good Morning Everyone 출력
// 클로저 표현식으로 작성(Inline으로 작성)
// 코드 블록으로 구성
func greeting() -> (() -> Void) {
return { () -> Void in
print("How are you")
}
} - 파라미터로 사용한다.파라미터 사용하기
// 원래 함수
func add(_ i : Int, _ j : Int) -> Int {
let sum = i + j
return sum
}
// 함수 타입 : (Int) -> Void
func add(_ i : Int, _ j : Int, _ handle : ((Int) -> Void)) {
let sum = i + j
handle(sum)
}
// 사용하기
add(3, 4, {(result : Int) -> Void in
print("3 + 4 = \(result)")
}) - 프로퍼티에 사용한다.프로퍼티에 사용
// 클래스 정의
// 함수 타입 : () -> Int
class MyClass {
var property : (() -> Int)!
}
// 사용하기
var obj = MyClass()
obj.propery = {
return 0
} - 클로저에서 프로퍼티 접근 : self를 사용하여 접근프로퍼티 접근
// 클래스 정의
// 함수 타입 : () -> Void
class MyClass {
var value = 0
func showAndPrint() -> (() -> Void) {
return {
print(self.value)
}
}
} - 클로저와 타입 알리아스
- 타입 알리아스는 타입에 이름을 주어 해당 타입 대신에 타입 알리아스로 정한 이름으로 대체가 가능하게 한 방법이다.타입 알리아스
// 타입 알리아스 정의
typealias Century = Int
// 타입 알리아스 사용
var thisEra : Century = 12 // Int 대신 사용 가능
// 클로저에서 타입 알리아스 활용
// 원래 함수
func add(_ i : Int, _ j : Int) -> Int {
let sum = i + j
return sum
}
// 함수 타입 : (Int) -> Void
func add(_ i : Int, _ j : Int, _ handle : ((Int) -> Void)) {
let sum = i + j
handle(sum)
}
// 타입 알리아스 정의
typealias ResultHandler = (Int) -> Void
// 함수 타입 : (Int) -> Void
func add(_ i : Int, _ j : Int, _ handle : ResultHandler) {
let sum = i + j
handle(sum)
}
// 타입 알리아스에 정의된 이름으로 대체 가능
Swift 구조체와 enum
- 구조체란? 프로그래밍 언어에서 의미가 연결된 한 덩어리로 처리하는 방식으로 클래스처럼 프로퍼티와 메소드로 구성된다.
- 구조체와 클래스의 관계
- 구조체와 클래스에 모두 있는 것
- 프로퍼티와 메소드
- 초기화(Initializer)
- 구조체에는 없고 클래스만 있는 것
- 상속
- 재정의
- ARC(Automatic Reference Count)
- 구조체와 클래스의 속성
- 구조체의 정의
- 정의 방법 : struct[구조체 이름]{ // 프로퍼티, 메소드 구현}
- 구조체의 객체 생성과 사용구조체의 객체 생성과 사용
// 구조체 정의
struct Point {
var x = 0
var y = 0
func description() -> String {
return "Point : \(x), \(y)"
}
}
// 구조체 생성
var point = Point()
point.x = 10
point.y = 20
// 해당 부분은 var point = Point(x : 10, y : 20) 가능
// => 구조체에서는 프로퍼티를 자동으로 초기화한다.
print(point.description()) - 구조체의 초기화(Initializer)
- 초기화가 필요한 프로퍼티 정의초기화가 필요한 프로퍼티 정의
// 구조체 정의
struct Point {
var x : Int
var y : Int
func description() -> String {
return "Point : \(x), \(y)"
}
}
// 구조체 생성
var point1 = Point()// 에러 발생
// init(x : Int, y : Int) 가 있는 것과 동일한 상태
var point2 = Point(x : 10, y : 20) - Initializer 정의구조체에 Initializer 정의
// 구조체 정의
struct Point {
var x : Int
var y : Int
init() {
self.x = 0
self.y = 0
}
}
// 구조체 생성
var point1 = Point()
var point2 = Point(x : 10, y : 20)// 에러 발생
// Initialize 정의 시는 프로퍼티 자동 초기화는 되지 않음 - 구조체의 프로퍼티 수정
- 초기화 메소드에서 설정 가능
- 구조체 외부에서 설정 가능
- 구조체 내부에서 변경 시 메소드 선언에 mutating 키워드 추가 후 설정 가능
- 구조체의 프로퍼티 수정은 아래와 같다.구조체의 프로퍼티 수정
// 구조체 정의
struct Point {
var x : Int
var y : Int
// 초기화 메소드 사용
init() {
self.x = 0
self.y = 0
}
// 구조체 내부에서 설정
mutating func moveTo(x : Int, y : Int) {
self.x = x
self.y = y
}
}
// 구조체 생성
var point = Point()
// 구조체 외부에서 설정
point.x = 10
point.y = 20 - Enum이란? 열거형 타입으로 원소를 정의하여 그 원소 내에서만 사용하는 데이터 타입의 일종으로 enumeration 에서 따왔다.
- Enum 정의Enum 정의
// Enum 정의
enum Pet {
case cat
case dog
case bird
}
enum Week : Int {
case sun, mon, tue, wed, thur, fri, sat
}
// Int 형으로 정의 시 sun은 0이며, 하나씩 증가하게 된다.
// mon = 1, tue = 2, ...
enum Day : String {
case am = "오전"
case pm = "오후"
}
// Int 형 이외에는 값을 반드시 넣어주어야 한다. - Enum 사용Enum 사용
// Enum 정의
enum Pet {
case cat
case dog
case bird
}
// enum 객체 정의
var pet : Pet
pet = Pet.cat
pet = Pet.dog
pet = Pet.other// 없는 값이므로 에러 발생
// enum은 주로 Switch 문과 사용
switch pet {
case Pet.cat :
print("고양이")
case .dog :
print("강아지")
case .bird :
print("새")
}
// enum의 원소의 값 얻기
pet.rawValue // dog를 얻을 수 있음
- Enum의 계산 프로퍼티와 메소드Enum 사용
// Enum 정의
enum Pet {
case cat
case dog
case bird
// 계산 프로퍼티 정의
var name : String {
switch self {
case .cat : return "고양이"
case .dog : return "강아지"
case .bird : return "새"
}
}
func description() -> String {
return self.name
}
}
// enum 객체 정의
var pet : Pet
pet = Pet.cat
print(pet.description()) // 고양이 출력
pet = Pet.dog
print(pet.description()) // 강아지 출력
Swift 메모리 관리
- 메모리 관리는 객체를 생성하면 메모리를 차지하게 되는데 메모리의 공간은 한계가 있기 때문에 필요한 객체가 차지하는 메모리는 유지하고 필요없는 객체의 메모리를 해제하여 공간을 효율적으로 사용하도록 하는 방법이다.
- 객체가 사용 중인지 여부는 레퍼런스 카운트(Reference Count)가 관리한다.
- 객체를 사용하게되면 레퍼런스 카운트가 증가되고, 사용이 끝나면 레퍼런스 카운트를 감소한다.
- 만약 레퍼런스 카운트가 0이 되면, 객체를 사용하는 부분이 없어 메모리에서 해제를 하는 방식으로 메모리를 관리한다.
- 레퍼런스 카운트는 이전에는 수동으로 관리를 했으나 현재는 ARC(Automatic Reference Count)를 이용하여 자동으로 관리한다.
- ARC(Automatic Reference Count)
- 객체의 레퍼런스 카운트 관리 코드를 자동으로 생성하여 관리한다.
- 레퍼런스 타입(클래스) 객체에만 적용한다.
- Value Type(구조체, enum) 객체에서는 적용되지 않는다.
- 객체의 메모리 소유와 해제
- 메모리 소유 : 객체 생성 시에 메모리를 소유하게 된다.객체 생성
class MyClass {
}
var ptr : MyClass! = MyClass()
// 객체를 생성하여 메모리를 소유한다. - 메모리 해제 : 객체에 nil을 대입하여 레퍼런스 카운트가 0이 되어 메모리에서 해제된다.객체 해제
var ptr : MyClass! = MyClass()
ptr = nil // 객체를 해제하여 메모리를 해제한다. - 강한 참조 : 객체에 nil을 대입하면 무조건 메모리가 해제되는 것이 아니라 레퍼런스 카운트가 0이되어 메모리가 해제되는 것이며, 다른 객체를 참조하고 있다면 레퍼런스 카운트가 0이 되지 않아 메모리 해제가 발생되지 않는다.강한 참조
var ptr : MyClass! = MyClass() // RC = 1
var anotherPointer = ptr // RC = 2
ptr = nil
// RC = 1로 레퍼런스 카운트가 0이 되지 않아 메모리에서 해제되지 않는다.
// RC = 0을 만들기 위해서 아래의 코드가 필요하다.
// anotherPointer = nil : RC = 0이되어 메모리 해제 발생 - 메모리 해제 확인은 deinit 메소드로 확인한다.메모리 해제 확인
class MyClass {
deinit {
print("메모리 해제")
}
} - 강한 참조로 순환 참조
- 앞에 강한 참조를 이용하여 서로 다른 클래스가 상대방을 소유하고 있다면 레퍼런스 카운트가 0이 되지 않아 메모리가 할당된 상태가 유지가 된다.
- 해당 상황을 표현한 그림은 아래와 같다.

- 위 상황을 보면 Car 클래스는 Engine 클래스의 객체를 생성하고, Engine 클래스는 Car 클래스의 객체를 생성하여 서로 상대방의 클래스의 객체를 생성하여 서로 레퍼런스 카운트를 증가시켜주고 있다.
- 코드로 나타내면 아래와 같이 된다.강한 순환 참조
class ClassA {
var objB : ClassB!
deinit {
print("ClassA 객체 해제")
}
}
class ClassB {
var objA : ClassA!
deinit {
print("ClassB 객체 해제")
}
}
var a : ClassA! = ClassA() // ClassA RC = 1
var b : ClassB! = ClassB() // ClassB RC = 1
a.objB = b // ClassB RC = 2
b.objA = a // ClassA RC = 2
a = nil
// ClassA RC = 1 : RC 가 0이 되지 않아서 메모리 할당유지
b = nil
// ClassB RC = 1 : RC 가 0이 되지 않아서 메모리 할당유지 - 약한 참조
- 강한 순환 참조는 서로 상대방 클래스의 RC 를 늘려 정작 본인 클래스의 메모리 해제 시에 RC 가 0이 되지 않아서 메모리 해제가 발생되지 않는데 객체를 소유하지 않는 약한 참조(weak, unowned)를 이용하여 강한 참조를 발생하지 않을 수 있다.
- weak
- 참조하던 객체가 해제가 되면 자동으로 nil이 되는 구조이다.
- nil이 되므로 옵셔널 타입으로 선언한다.
- 상호 독립적으로 존재하는 객체에 사용한다.
- 사용 방법약한 참조 : weak
// Person과 Phone이 서로 독립적인 존재
class Person {
weak var phone : Phone!
deinit {
print("Person 객체 해제")
}
}
class Phone {
var person : Person!
deinit {
print("Phone 객체 해제")
}
}
var person : Person! = Person() // Person RC = 1
var iphone : Phone! = Phone() // Phone RC = 1
// 순환참조
iphone.person = person // Person RC = 2
person.phone = iphone
// Phone RC = 1 : 약한참조로 소유하지 않음
// 객체 해제 확인
person = nil // Person RC = 1
iphone = nil // Phone RC 0, Person RC = 0
/* Person, Phone 클래스 모두 레퍼런스 카운트가 0이 되어 메모리 해제 발생 */ - unowned
- 참조하던 객체가 해제가 되어도 nil로 변하지 않아 Dangling Pointer의 위험이 있다.
- 옵셔널 타입으로 선언할 수 없어 Initializer 가 필요하다.
- 완전히 종속적인 경우에 사용한다.
- 사용 방법약한 참조 : unowned
class Country {
var capital : City!
}
class Capital {
unowned var country : Country
init(country:Country) {
self.country = country
}
}
var korea : Country! = Country() // Country RC = 1
var seoul : Capital! = Capital(country:korea)
// Country RC = 1 : 약한 참조로 소유하지 않음
// Capital RC = 1
// 순환 참조
korea.capital = seoul
// Capital RC = 2
// 객체 해제 확인
korea = nil // Country RC = 0
seoul = nil
// Capital RC = 1, Country RC = 0
// Capital RC = 0
/* Country, Capital 클래스 모두 레퍼런스 카운트가 0이 되어 메모리 해제 발생 */
Swift 상속
- 상속이란? 기존(부모) 클래스에 구현된 변수(프로퍼티)/메소드를 상속받은 새로운(자식) 클래스가 그대로 사용가능한 상태를 의미한다.
- Swift 클래스 상속
- 클래스의 상속은 단일 상속만 허용한다. 즉 자식 클래스는 하나의 부모 클래스만 상속한다.
- 문법클래스 상속 문법
class Child Class Name : Parent Class Name {
} - 클래스 상속과 포인터
- self, super
- self 포인터는 현재 자신의 클래스 내에 있는 프로퍼티/메소드에 접근 시에 사용한다.
- super 포인터는 자신이 상속하고 있는 부모 클래스의 프로퍼티/메소드 접근 시에 사용한다.
- self 포인터와 super 포인터 사용 예시는 아래와 같다.self 포인터와 super 포인터
// 부모 클래스
class Parent {
func description() -> String { return "Parent Class" }
}
// 자식 클래스
class Child {
// 메소드 재정의
override func description() -> String {
return "Child Class"
}
func printDescription() {
print("super.description : \(super.description())")
// "Parent Class" 출력
print("self.description : \(self.description())")
// "Child Class" 출력
}
} - 클래스 상속과 재정의
- 클래스 상속은 부모 클래스의 프로퍼티/메소드를 자식 클래스에서 그대로 사용 가능하다.
- 메소드 재정의
- 부모 클래스에 있는 상속받은 메소드(같은 이름의 메소드)를 자식 클래스에서 다른 동작을 하도록 다시 정의하는 것을 메소드 재정의 라고 한다.
- 메소드 재정의는 메소드 앞에 override 키워드를 이용하여 재정의한다.메소드 재정의
// 부모 클래스
class Parent {
var value = 0
func hello() {
print("Hello Parent Class")
}
}
// 자식 클래스
class Child : Parent { // Parent 클래스 상속
override func hello() { // 동일 이름의 다른 동작
print("Hello Child Class")
}
}
- 프로퍼티 재정의
- 부모 클래스에 정의된 프로퍼티를 재정의 한다.
- 프로퍼티 재정의 시에도 메소드 재정의와 동일하게 앞에 override 키워드를 이용한다.
- 저장 프로퍼티 재정의
- 방법 : willSet/didSet 행위를 추가해준다.저장 프로퍼티 재정의
// 사각형의 부모 클래스
class Rectangle {
var width = 0
var height = 0
var size : Int {
get{return width * height}
}
}
// 정사각형의 자식 클래스
class Square : Rectangle { // Rectangle 클래스를 상속
override var width : Int {
didSet(newValue) {
// 새로 추가되는 행위
self.width += newValue
}
}
} - 계산 프로퍼티 재정의
- 방법 : get/set 행위를 재정의해준다.계산 프로퍼티 재정의
// 사각형의 부모 클래스
class Rectangle {
var width = 0
var height = 0
var size : Int {
get{return width * height}
}
}
// 정사각형의 자식 클래스
class Square : Rectangle { // Rectangle 클래스를 상속
override var size : Int {
get {
height = width
return width * height
}
}
} - 재정의 시 주의사항
- 재정의가 필요한 프로퍼티/메소드에 override 키워드 누락 시에 에러 발생
- 재정의하는 대상이 아닌 프로퍼티/메소드에 override 키워드 붙이는 경우 에러 발생
- 부모 클래스의 프로퍼티/메소드 앞에 final 키워드가 붙은 경우는 재정의가 금지된 것으로 자식 클래스에서 재정의 할 수 없다.
- 클래스 상속과 초기화
- 자식 클래스는 부모 클래스의 프로퍼티를 상속하게 된다.
- 부모 클래스에 초기화가 필요한 프로퍼티가 있는 경우, 자식 클래스에서의 초기화는 아래와 같다.
- 자식 클래스에 Designated Initializer 가 없는 경우 부모 클래스의 Designated, Convenience Initializer 모두 상속한다.부모 클래스의 Initializer 상속
// 부모 클래스
class Parent {
var a : Int
init(a : Int) {
self.a = a
}
convenience init() {
self.init(a : 10)
}
}
// 자식 클래스
class Child : Parent {
// 부모 클래스에 있는 Initializer 를 모두 상속
}
// 객체 생성
var ChildObj1 = Child(a : 20) // Designated Initializer 상속
var ChildObj2 = Child() // Convenience Initializer 상속 - 자식 클래스에 Designated Initializer 가 있는 경우 부모 클래스의 Designated, Convenience Initializer 모두 상속하지 못한다.부모 클래스의 Initializer 상속 못함
// 부모 클래스
class Parent {
var a : Int
init(a : Int) {
self.a = a
}
convenience init() {
self.init(a : 10)
}
}
// 자식 클래스
class Child : Parent {
var b : Int
init(a : Int, b : Int) {
self.b = b
super.init(a : a)
}
/* 자식 클래스에 Designated Initializer 가 있는 경우 부모 클래스의 Initializer 를 상속받지 못함 */
}
// 객체 생성
// 자식 클래스의 Designated Initializer
var ChildObj1 = Child(a : 20, b : 30)
// 부모 클래스의 Initializer 를 상속 받지 못함
var ChildObj2 = Child(a : 20)
var ChildObj3 = Child() - 자식 클래스에서 Designated Initializer 를 정의하지 않고, Convenience Initializer를 부모 클래스의 Designated Initializer를 상속부모 클래스의 Designated Initializer 상속
// 부모 클래스
class Parent {
var a : Int
init(a : Int) {
self.a = a
}
convenience init() {
self.init(a : 10)
}
}
// 자식 클래스
class Child : Parent {
var b : Int
convenience init(a : Int, b : Int) {
self.b = b
super.init(a : a)
}
/* 자식 클래스에 Designated Initializer 가 없고 Convenience Initializer 만 정의한 경우 부모 클래스의 Initializer를 모두 상속 */
}
// 객체 생성
// 자식 클래스의 Designated Initializer
var ChildObj1 = Child(a : 20, b : 30)
// 부모 클래스의 Initializer 를 상속 받음
var ChildObj2 = Child(a : 20)
var ChildObj3 = Child() - 자식 클래스에 부모 클래스에 있는 모든 Designated Initializer를 재정의한 경우, Convenience Initializer 를 상속한다.부모 클래스의 Designated Initializer 모두 상속
// 부모 클래스
class Parent {
var a : Int
init(a : Int) {
self.a = a
}
convenience init() {
self.init(a : 10)
}
}
// 자식 클래스
class Child : Parent {
override init(a : Int) {
self.b = 20
super.init(a : a)
}
/* 부모 클래스에 Designated Initializer 를 모두 재정의한 경우 부모 클래스의 Convenience Initializer 를 상속 */
}
// 객체 생성
var ChildObj = Child()
// 부모 클래스의 Convenience Initializer 상속 - 클래스 초기화 관계는 아래와 같다.

- 상속과 Failable Initializer
- 부모 클래스의 Failable Initializer를 자식 클래스가 위임한다.
- 자식 클래스에서 부모 클래스의 Failable Initializer 재정의하는 방법은 아래와 같다.
- Failable Initializer를 Failable Initializer로 재정의하는 방법Failable Initializer를 Failable Initializer로 재정의
// 부모 클래스
class Parent {
var value : Int
// Failable Initializer
init?(value : Int) {
if(value < 0) {
return nil
}
self.value = value
}
// Non-Failable Initializer
init() {
self.value = 0
}
}
// 자식 클래스
class Child : Parent {
override init?(value : Int) {
// Failable Initializer로 위임
super.init(value : value)
}
} - Failable Initializer를 Non-Failable Initializer로 재정의하는 방법Failable Initializer를 Non-Failable Initializer로 재정의
// 부모 클래스
class Parent {
var value : Int
// Failable Initializer
init?(value : Int) {
if(value < 0) {
return nil
}
self.value = value
}
// Non-Failable Initializer
init() {
self.value = 0
}
}
// 자식 클래스
class Child : Parent {
override init(value : Int) {
// Non-Failable Initializer로 위임
super.init()
}
} - Non-Failable Initializer를 Failable Initializer로 재정의는 할 수 없다.
Swift 객체 초기화
- 객체 초기화 : 모든 객체는 사용하기 전에 반드시 클래스 내부에서 사용하는 값에 대한 초기화를 해야한다.
- 프로퍼티 초기화
- 자동 초기화
- 옵셔널 타입을 이용한 프로퍼티 초기화 : var optionalValue : Int?
- 초기값이 있는 프로퍼티 초기화 : var initalValue : Int = 0
- 수동 초기화
- 옵셔널 타입이 아니고 초기값이 없는 프로퍼티 : var manualInitValue : Int
- 객체 초기화 코드인 Initializer(init 메소드)를 작성하여 프로퍼티를 초기화한다.Initializer를 이용한 초기화
class Rectangle {
var width : Int
var height : Int
// init 메소드에 초기화 진행
override init() {
width = 0
height = 0
}
}
// Initializer 형태에 맞게 객체 생성
let rect = Rectangle() - Initializer : 수동 초기화에서 보았듯이 초기화가 필요한 프로퍼티를 초기화하며 init 메소드를 이용한다.
- Initializer 종류
- Designated Initializer
- 객체 초기화를 단독으로 완료 가능
- 모든 초기화가 필요한 프로퍼티 초기화
- 클래스 내에서 반드시 1개 이상 필요
- 사용 방법Designaged Initializer 사용법
// 모든 값을 초기화
class Rectangle {
var width : Int
var height : Int
// init 함수에 초기화 진행
// Designated Initializer 1
override init() {
width = 0
height = 0
}
// Designated Initializer 2
init(width : Int, height : Int) {
self.width = width
self.height = height
}
}
// Initializer 형태에 맞게 객체 생성
// Designated Initializer 1 로 객체 생성
let rect1 = Rectangle()
// Designated Initializer 2 로 객체 생성
let rect2 = Rectangle(width : 10, height : 20) - Convenience Initializer
- 단독으로 초기화 불가능
- 일부 프로퍼티만 초기화
- 다른 Initializer(Designated Initializer)를 이용해서 초기화
- 중복 코드 방지
- 사용 방법Convenience Initializer 사용법
class Rectangle {
var width : Int
var height : Int
// Designated Initializer 1
override init() {
width = 0
height = 0
}
// Designated Initializer 2
init(width : Int, height : Int) {
self.width = width
self.height = height
}
// Convenience Initializer
init(height : Int) {
self.init() // Designated Initializer 1 반드시 필요
self.height = height
}
}
// Initializer 형태에 맞게 객체 생성
// Designated Initializer 1 로 객체 생성
let rect1 = Rectangle()
// Designated Initializer 2 로 객체 생성
let rect2 = Rectangle(width : 10, height : 30)
// Convenience Initializer 로 객체 생성
var rect3 = Rectangle(height : 20) - Failable Initializer : 객체 생성 시 초기화 실패 시에 nil로 처리하는 방법이다.
- 작성 방법은 Initializer와 동일하고, 오류 상황에 대한 조건을 확인하여 nil로 반환해준다.
- 옵셔널 타입을 이용하여 Initializer 작성옵셔널 타입을 이용하여 Initializer 작성
class Rectangle {
var width : Int
var height : Int
// Designated Initializer 1
override init() {
width = 0
height = 0
}
// Designated Initializer 2
init?(width : Int, height : Int) {
// Failable Initializer 작성
guard(width > 0 && height >0 ) else {
return nil
}
self.width = width
self.height = height
}
}
// Initializer 형태에 맞게 객체 생성
// Designated Initializer 2 로 객체 생성
var rect1 = Rectangle(width : 10, height : 30)
// 정상 실행
var rect2 = Rectangle(width : -19, height : 30) // nil 반환 - 강제 언래핑을 이용하여 Initializer 작성Convenience Initializer 사용법
class Rectangle {
var width : Int
var height : Int
// Designated Initializer 1
override init() {
width = 0
height = 0
}
// Designated Initializer 2
init!(width : Int, height : Int) {
// Failable Initializer 작성
guard(width > 0 && height >0 ) else {
return nil
}
self.width = width
self.height = height
}
}
// Initializer 형태에 맞게 객체 생성
// Designated Initializer 2 로 객체 생성
var rect1 : Rectangle! = Rectangle(width : 10, height : 30)!
// 정상 실행
var rect2 : Rectangle! = Rectangle(width : -10, height : 30)! // nil이 반환되므로 런타임 에러 발생 - 객체 해제 : 객체가 메모리에서 해제되는 것을 의미한다.
- 객체 해제 시에 deinit 메소드가 호출된다.
- deinit 메소드는 파라미터, 리턴 타입이 없다.
- deinit 메소드에 객체가 해제 되면서 동작할 코드를 작성한다.
- 사용 방법deinit 메소드 사용법
class Rectangle {
var width : Int
var height : Int
// 객체 초기화
override init() {
width = 0
height = 0
}
// 객체 해제 시 호출
deinit {
// 객체 해제 시 동작할 코드 작성
}
}
// Initializer 형태에 맞게 객체 생성
/* 객체 해제 시 nil 값을 넣어야 하므로 옵셔널 타입 또는 암시적 언래핑으로 호출*/
var rect1 : Rectangle? = Rectangle() // 옵셔널 타입
var rect2 : Rectangle! = Rectangle() // 암시적 언래핑
// 객체 해제되면서 deinit 메소드를 호출
rect1 = nil
rect2 = nil
// 위와 같이 객체 해제하려면 반드시 옵셔널 타입이나 암시적 언래핑으로 호출
Swift 클래스
- 클래스를 이해하기 전에 먼저 객체 지향 프로그램에 대해서 이해가 필요하다.
- 해당 내용은 아래 링크에 자세하게 다루었다.
- 정리하자면 객체 지향 프로그래밍의 특징은 아래와 같다.
- 모든 것을 객체로 취급한다.
- 객체는 실 세계에 존재하거나 생각할 수 있는 것들을 통틀어서 이야기한다.
- 클래스로 정의되고 클래스의 속성(변수, 데이터, 프로퍼티)와 클래스의 메소드 형태로 구성된다.
- 클래스의 메소드는 행위를 나타낸다.
- 클래스 정의와 객체 생성 방법
- 클래스 정의 : class [클래스 명] { 클래스의 속성 및 메소드 }클래스 정의
class Rectangle {
// 사각형의 가로, 세로 길이에 대한 속성을 정의
// 사각형의 넓이에 대한 메소드를 정의
} - 객체 생성 : [클래스 명]()객체 생성
var rect = Rectangle() // 새로운 객체 대입 가능
rect = Rectangle()
let rect1 = Rectangle() // 새로운 객체 대입 불가능 - 소스코드 파일과 클래스의 관계
- 클래스와 프로퍼티
- 프로퍼티는 객체의 데이터를 나타내며 값을 저장하고 읽는 역할을 한다.
- 프로퍼티 종류 (저장 프로퍼티, )
- 저장 프로퍼티
- 데이터를 저장하는 용도로 쓰인다.
- 데이터를 읽고 쓰는 역할을 한다.
- 객체 생성 시 초기화하며, 초기화하는 방법이 필요하다.
- 저장 프로퍼티 정의 및 사용 예시는 아래와 같다.저장 프로퍼티 예시
// 저장 프로퍼티 정의
class MyClass{
// 초기값을 설정한 프로퍼티
var intProperty = 0
// 초기값을 설정하지 않은 프로퍼티
var floatProperty : Float?
// 초기값 설정이 반드시 필요한 프로퍼티
var strProperty : String = "Test"
}
// 저장 프로퍼티 사용
var obj = MyClass() // 객체 생성
// 저장 프로퍼티에 값을 저장
obj.intProperty = 10
obj.floatProperty = 3.1
// 저장 프로퍼티에 값을 사용
let val = obj.floatProperty
print("val = \(val)") // val = 3.1 출력 - 계산 프로퍼티
- 데이터를 저장하지 않는다.
- 프로퍼티 읽기/쓰기 코드를 작성한다.(set/get)
- 읽기전용은 가능하고 쓰기 전용은 사용할 수 없다.(get만 존재)
- 계산 프로퍼티 쓰기에서 설정 값은 반드시 한개만 가능하다.(set에 파라미터는 한개만 가능하다.)
- set에 들어가는 기본 파라미터 명은 newValue 이며, 그대로 사용해도 되고, 변경도 가능하다.
- 계산 프로퍼티 정의 및 사용 예는 아래와 같다.계산 프로퍼티 예시
// 계산 프로퍼티 정의
class Rectangle {
var width = 0
var height = 0
// 계산 프로퍼티 정의 (읽기 전용)
var area : Int {
get {
return (width * height)
}
}
}
// 계산 프로퍼티 사용
var rect = Rectangle()
rect.width = 10
rect.height = 20
print(rect.area) // 200 출력 - 프로퍼티 변경 감시 : 프로퍼티가 객체 생성 후에 변경이 될 경우 변경 전의 값과 변경 후의 값을 확인할 수 있다.
- 프로퍼티 변경 전 : willSet
- 프로퍼티 변경 후 : didSet
- 프로퍼티 변경 감시 사용 예시는 아래와 같다.프로퍼티 감시 예시
class Rectangle {
var height : Int = 0 {
willSet {
print("사각형 높이 변경 예정 : \(newValue)")
}
didSet {
print("사각형 높이 변경 완료. 이전 값 : \(oldValue)")
}
}
} - 프로퍼티의 늦은 초기화 : 보통은 객체 생성과 동시에 프로퍼티를 초기화를 하지만 프로퍼티를 사용할 때 초기화하는 방법이 있다.
- 프로퍼티의 늦은 초기화하는 lazy 키워드를 앞에 붙여서 정의한다.
- 사용 예시는 아래와 같다.프로퍼티의 늦은 초기화
class Person {
lazy var phone = Phone()
}
let john = Person()
john.phone // 이 때 초기화가 된다. - 클래스와 메소드
- 클래스 내에서 메소드(함수)는 어떤 동작을 할지 정의한다.
- 메소드의 종류
- 인스턴스 메소드
- 객체 생성 후 사용해야한다. (객체를 생성하지 않고는 사용할 수 없다.)
- 객체의 데이터(프로퍼티) 접근이 가능하다.
- 타입 메소드
- 객체를 생성하지 않고 사용 가능하다.
- 객체의 데이터(프로퍼티) 접근을 할 수 없다.
- 키워드는 static으로 한다.(자세한 내용은 5번에서 다룬다.)
- 이번에 다루는 메소드는 인스턴스 메소드이다.
- 클래스 내에서 메소드 정의 및 사용 예시는 아래와 같다.클래스 내에서 메소드 정의 및 사용
// 클래스 내에 메소드 정의
class Counter {
var count = 0
// 파라미터가 없는 메소드 정의
func increment() {
count += 1
}
// 파라미터가 1개 있는 메소드 정의
func increment(amount : Int) {
count += amount
}
// 파라미터가 2개 있는 메소드 정의
func increment(amount : Int, times : Int) {
count += (amount * times)
}
}
// 정의된 메소드 사용
let counter = Counter()
counter.increment() // count가 1로 증가
counter.increment(amount : 5) // counter가 6으로 증가
counter.increment(amount : 5, times : 3)
// counter가 21로 증가 - self 포인터
- 클래스 내에서 객체 자신을 참조하는 포인터로 프로퍼티 이름과 메소드의 파라미터 이름이 겹칠 때 프로퍼티 이름과 파라미터 이름을 구분하기 위해서 사용한다.self 포인터 사용
class Counter {
var count = 0 // 프로퍼티
// 메소드
func setCount(count : Int) { // 메소드의 파라미터
self.count = count
}
} - 클래스의 타입 메소드와 타입 프로퍼티
- 타입 메소드와 타입 프로퍼티는 모두 객체 생성 없이 사용이 가능하다.
- 타입 메소드는 클래스 내에 정의된 프로퍼티에는 접근할 수가 없다.
- 타입 메소드와 타입 프로퍼티는 static을 앞에 붙여서 정의한다.
- 타입 메소드, 타입 프로퍼티 사용 예시는 아래와 같다.타입 메소드, 타입 프로퍼티 정의 및 사용
// 클래스 내에 타입 메소드, 타입 프로퍼티 정의
class Rectangle {
var width = 0
var height = 0
// 사각형에 대한 정보를 타입 프로퍼티로 정의
static var edge = 4
var area {
get {
return (width * height)
}
}
// 이름에 대한 정보를 타입 메소드로 정의
static func name() -> String {
return "사각형"
}
}
// 정의된 타입 메소드, 타입 프로퍼티 사용
var rect = Rectangle()
rect.width = 10
rect.height = 20
// 타입 프로퍼티 접근
Rectangle.edge // 4
// 타입 메소드 접근
Rectangle.name() // 사각형 - 클래스와 연산자
- 연산자 : 클래스 내에 메소드를 정의하는 것과 동일하게 연산자를 이용하여 해당 클래스 내에서 전용으로 사용되는 연산자를 정의하여 사용할 수 있다. 예) + 연산자에 추가 기능을 더할 수 있다.
- 연산자 정의 시 필요 내용
- 피 연산자의 개수는 파라미터의 개수와 동일하다.
- 단항 연산자는 파라미터 개수가 1개
- 이항 연산자는 파라미터 개수가 2개
- 연산자의 위치
- 앞에 위치 : 전위 연산자 - prefix
- 중간에 위치(기본값) : 중위 연산자 - infix
- 뒤에 위치 : 후위 연산자 - postfix
- 연산자 함수 정의
- 연산자 함수는 타입 메소드로 작성
- 연산자 함수의 이름은 연산자 예) +, -
- 단항 연산자는 연산자 위치 정보가 필요 (prefix, infix, postfix)
- 사용 방법연산자 선언 및 사용
// 클래스 내에 연산자 선언
class Point {
var x : Int
var y : Int
// Initializer
init(x : Int, y : Int) {
self.x = x
self.y = y
}
// 단항 연산자 선언
static prefix func -(point : Point) -> Point {
var another = Point()
another.x = -point.x
another.y = -point.y
return another
}
// 이항 연산자 선언
static func +(left : Point, right : Point) -> Point {
return Point(x : (left.x + right.x), y : (left.y + right.y))
}
}
var point1 = Point(x : 10, y : 20)
point2 = -point1 // Point(x : -10, y : -20)
let result = point1 + point2 // Point(x : 0, y : 0)
Swift 함수
- 함수란? 애플리케이션에서의 동작의 기본 단위를 의미한다.
- 함수 작성 방법함수 정의 및 사용
1. 함수 정의
func greeting() {
print("Hello Swift")
}
2. 함수 사용
greeting() - 함수의 실행 결과 반환 : 반환 타입 중 Void는 생략 가능 그 외에는 모두 "-> 반환타입"으로 명시한다.함수의 실행 및 반환
1. 함수 정의
func greeting() -> String {
return "Hello Swift"
}
2. 함수 사용
var str = greeting()
print(str) // Hello Swift 출력 - 함수 파라미터 : 함수 동작에 필요한 정보를 전달하는데 사용된다.
- 함수 파라미터 정의함수 파라미터 정의
1. 파라미터 1개
func greeting(person : String) {
print("Hello \(person)")
}
2. 파라미터 2개
func greeting(person : String, emotion : String) {
print("Hello \(person) with \(emotion)")
} - 파라미터 이름의 종류
- 내부 파라미터 : 함수 내부에서 접근 및 사용한다.함수 내부 파라미터
1. 내부 파라미터
func greeting(person : String) {
print("Hello \(person)")
}
// 내부 파라미터는 함수 내부에서 접근 및 사용에 쓰인다. - 외부 파라미터 : 함수 내부에서 접근 및 사용할 수 없으며, 외부에서 함수로 필요한 정보를 전달하는 역할을 한다.함수 외부 파라미터
1. 내부 파라미터와 외부 파라미터의 이름이 동일
func greeting(person : String) {
print("Hello \(person)")
}
greeting(person : "My Friend") // Hello My Friend 출력
// 외부 파라미터는 함수 내부로 필요한 정보를 전달 시에 사용
2. 외부 파라미터 이름을 수동을 설정
func greeting(who person : String) {
print("Hello \(person)")
}
greeting(who : "Jun") // Hello Jun 출력 - 파라미터 기본값 설정 방법 : 기본값 설정은 생략 가능하며, 함수 사용 시에 외부에서 함수로 넘겨주는 값이 없는 경우에만 사용된다.파라미터 기본값 설정
func greeting(person : String = "Mom") {
print("Hello \(person)")
}
greeting(person : "My Friend") // Hello My Friend 출력
greeting() // Hello Mom 출력 - 파라미터는 함수 내부에서 상수로 사용하여 변경 시 에러가 발생하는데 변경이 필요한 경우는 inout 키워드로 변경가능하도록 한다.함수 내부에서 파라미터 변경
func tryParamValueChange1(arg : Int = 0) {
arg = arg + 1 // 에러 발생
}
func tryParamValueChange2(arg : inout Int = 0) {
arg = arg + 1 // 에러 발생하지 않고 변경 가능
}
tryParamValueChange2(1) // 2로 변경
func swapTwoValue(_ arg1 : inout Int, _ arg2 : inout Int) {
let temp = arg1
arg1 = arg2
arg2 = temp
}
var value1 = 1
var value2 = 2
swapTwoValue(&value1, &value2)
// value1에는 2, value2에는 1로 변경되며 &로 사용해야 변경 - 함수와 옵셔널 : 함수의 반환 타입, 함수 파라미터에 옵셔널로 선언되면 nil 사용이 가능하다.
- 함수의 반환 타입에 옵셔널 선언 : 반환 타입에 nil 반환이 가능하다.함수 반환 타입에 옵셔널 선언
func nilReturnFunction() -> Int? {
return nil
}
if let ret = nilReturnFunction() {
print("실행 결과 : \(ret)") // 실행 결과 : nil 출력
} - 함수 파라미터에 옵셔널 선언 : 파라미터 사용 전에 반드시 nil 여부를 판단해서 사용하도록 한다. (nil 여부 판단은 if 문이나 guard 문을 이용한다.)함수 파라미터에 옵셔널 선언
func greeting(person : String, emotion : String?) {
guard let str = emotion else {
print("Hello \(person)") // emotion이 nil 인 경우 출력
return
}
print("Hello \(person) with \(emotion)")
// emotion이 nil이 아닌 경우 출력
}
greeting(person : "My Friend", emotion : "play")
// Hello My Friend with play 출력
greeting(person : "Mom", emotion : nil)
// Hello Mom 출력
피드 구독하기:
글 (Atom)


