[새싹_iOS] 5주차

임승섭·2023년 8월 19일
0

새싹 iOS

목록 보기
13/45

Enum + 프로퍼티

  • 열거형은 컴파일 타임에 확정되기 때문에 인스턴스 프로퍼티/메서드 를 사용하기 어렵다
  • 타입 프로퍼티는 메모리 상의 데이터 영역(코힙스)에 올라가기 때문에 사용이 가능하다
    enum Nasa: String, CaseIterable {
    **	let baseURL = "https://apod.nasa.gov/apod/image/"	// 불가능
        static let baseURL = "https://apod.nasa.gov/apod/image/"	// 가능
    }
  • 그렇다고 무조건 인스턴스 프로퍼티를 사용할 수 없는 건 아니다
    • 인스턴스 연산 프로퍼티 : 값을 저장하지 않고, 값을 사용할 수 있는 통로로서의 역할
    • 즉, 이걸 호출하는 곳에서 메모리를 차지하고,
      enum 틀에서는 따로 메모리를 차지하지 않기 때문에 사용이 가능하다
    • rawValue와 다른 타입의 값을 받아올 때 유용하게 사용함
    enum Nasa: String, CaseIterable {
        var test: URL {
            return URL(string: "https://www.naver.com")
        }
    }
  • 또 그렇다고 무조건 인스턴스 연산 프로퍼티를 막 사용할 수는 없다
    • 타입 저장 프로퍼티는 타입 연산 프로퍼티에서만 사용 가능하다
    • 당연한 얘기인 듯 하지만
    • 인스턴스 연산 프로퍼티에서 타입명을 호출해서 타입 저장 프로퍼티를 사용하는 건 가능하다
    • 두 가지 경우를 비교해보자
/* 타입 저장 프로퍼티를 그냥 사용 */
enum Nasa: String, CaseIterable {
	static let baseURL = "https://apod.nasa.gov/apod/image/"
    
    // static 빼고 인스턴스 프로퍼티로 사용하려면 -> 에러 발생
    static var photo: URL {
    	return URL(string: baseURL + self.allCases.randomElement()!.rawValue)!
    }
}


/* 타입명을 통해 타입 프로퍼티 사용 */
enum Nasa: String, CaseIterable {
	static let baseURL = "https://apod.nasa.gov/apod/image/"
    
    var photo: URL {
    	return URL(string: Nasa.baseURL + self.allCases.randomElement()!.rawValue)!
    }
}

GCD

Download Image (sync vs. async)

  • 여러 이미지를 불러오는 작업

syncButtonClicked()

  • 동기 -> 하나의 작업이 끝날 때까지 다른 작업 시작할 수 없다

  • 버튼을 누르면, 작업이 모두 끝날 때까지 버튼이 눌려있는 상태이다

  • 순서가 맞춰지고, 끝나는 시점을 파악할 수 있다

    @objc func syncButtonClicked() {
            print("sync start")
            downloadImage(imageView: firstImageView, value: "first")
            downloadImage(imageView: secondImageView, value: "second")
            downloadImage(imageView: thirdImageView, value: "third")
            downloadImage(imageView: fourthImageView, value: "fourth")
            print("sync end")
    }
    
    func downloadImage(imageView: UIImageView, value: String) {
    
            print("===1===\(value)===")
            let data = try! Data(contentsOf: Nasa.photo)
            imageView.image = UIImage(data: data)
            print("===2===\(value)===")        
    }
  • 이미지가 화면에 보여지기 전까지 버튼이 눌린 상태 -> 다른 작업 불가

  • 순서 o, 끝나는 시점 o

  • 실행시키면 보라색 에러가 뜬다

asyncButtonClicked()

  • 비동기 -> 알바생들한테 일 뿌리고 작업 기다리지 않는다 -> 순식간에 프린트가 찍힌다

  • 오래 걸리는 작업(let data = try! ~)을 알바생한테 던진다

  • UI 관련 작업은 다시 main으로 가져온다

    @objc func asyncButtonClicked() {
            print("async start")
            asyncDownloadImage(imageView: firstImageView, value: "first")
            asyncDownloadImage(imageView: secondImageView, value: "second")
            asyncDownloadImage(imageView: thirdImageView, value: "third")
            asyncDownloadImage(imageView: fourthImageView, value: "fourth")
    
            print("async end")
    }
    
    func asyncDownloadImage(imageView: UIImageView, value: String) {
    
            print("===1===\(value)===", Thread.isMainThread)
            DispatchQueue.global().async {	
                print("===2 - 1===\(value)===", Thread.isMainThread)
                let data = try! Data(contentsOf: Nasa.photo)
    
                DispatchQueue.main.async {  // 이게 sync일 때와 async일 때의 차이는 뭘까?
                    imageView.image = UIImage(data: data)
                    print("===2 - 2===\(value)===", Thread.isMainThread)
                }
                print("===2- 3===\(value)===")   
            }
            print("===3===\(value)===")        
    }
  • 순서가 랜덤이고, 끝나는 시점 파악할 수 없다 (-> DispatchGroup)

main.async vs. main.sync

  • 중간에 UI작업을 메인으로 던져줄 때, async와 sync로 쓸 때의 차이점이 궁금했다
  • 생각해보면 간단하긴 하네
  • sync로 던지면, 그 작업 기다렸다가 다음 일 하는거니까, global에서도 똑같이 적용된다



print(1~10) & print(11~20)

main.sync

  • 1~10까지 그냥 출력, 11~20 main.sync로 출력

    func serialSync() {
        print("start")
    
        for i in 1...10 {
            sleep(1)
            print(i, terminator: " ")
        }
    
        DispatchQueue.main.sync {
            for i in 11...20 {
                sleep(1)
                print(i, terminator: " ")
            }
        }
    
        print("end")
    }
    • 어차피 main.sync는 다시 main한테 던져주는 거니까,
      순서 그대로 출력된다
    • 라고 생각했는데,
      일을 기다려야 하는 main
      일을 줘야 하는 queue 사이에서
      도르마무 무한 대기상태가 발생하게 된다 (deadlock)
  • 그래서 이거 잘 안쓴다

main.async

  • 1~10을 main.async로 던져준다

  • 결국 main이 일해야 하는 것이므로 작업 순서만 바뀐다

    func serialAsync() {
        print("start")
    
        DispatchQueue.main.async {
            for i in 1...10 {
                sleep(1)
                print(i, terminator: " ")
            }
        }
    
        for i in 11...20 {
            sleep(1)
            print(i, terminator: " ")
        }
    
        print("end")
    }

global.sync

  • 1~10을 알바생한테 던지긴 하는데, 결국 보낸 일이 다 끝나야 main이 일 할 수 있다

    func globalSync() {
        print("start")
    
        DispatchQueue.global().sync {
            for i in 1...10 {
                sleep(1)
                print(i, terminator: " ")
            }
        }
    
        for i in 11...20 {
            sleep(1)
            print(i, terminator; " ")
        }
    
        print("end")
    }

global.async (1)

  • 동시에 작업이 끝난다

  • 작업이 빠르게 끝난다 라는게 중요하다 -> 약간 파이프라인 같은 느낌..?

  • 네트워크 통신처럼 오래 걸릴 수 있는 작업들을 보통 global.async로 던진다

    func globalAsync() {
        print("start")
    
        DispatchQueue.global().async {
            for i in 1...10 {
                sleep(1)
                print(i, terminator: " ")
            }
        }
    
        for i in 11...20 {
            sleep(1)
            print(i, terminator: " ")
        }
    
        print("end")
    }

global.async (2)

  • loop 안에서 도는 작업들을 각자 알바생한테 뿌린다

  • 엄청나게 빨라진다

    func globalAsync2() {
        print("start")
    
        for i in 100...300 {
            DispatchQueue.global().async {
                sleep(1)
                print(i, terminator: " ")
            }
        }
    
        for i in 1...20 {
            sleep(1)
            print(i, terminator: " ")
        }
    
        print("end")
    }

  • 작업 중인 쓰레드가 확 많아졌다가 확 줄어든다

DispatchGroup

  • 여러 비동기 코드가 끝나는 시점을 파악한다
  • qos : quality of service
  • 비동기로 맡기는 코드가 동기냐 비동기냐에 따라 약간 차이가 생긴다

동기비동기

  • 비동기로 맡기는 코드가 동기이다

    func dispatchGroup() {
    
        let group = DispatchGroup()
    
        DispatchQueue.global().async(group: group) {
            for i in 1...10 {
                print(i, terminator: " ")
            }
        }
    
        DispatchQueue.global().async(group: group) {
            for i in 11...20 {
                print(i, terminator: " ")
            }
        }
    
        DispatchQueue.global().async(group: group) {
            for i in 21...30 {
                print(i, terminator: " ")
            }
        }
    
        DispatchQueue.global().async(group: group) {
            for i in 31...40 {
                print(i, terminator: " ")
            }
        }	
    
        // 첫 번째 매개변수 : 신호를 받을 때, 어떤 쓰레드로 받을지 (보통 .main)
        // 두 번째 매개변수 : 클로저로 줌
        group.notify(queue: .main) {
            print("end")
        }
    }
  • 예상대로 끝나는 시점을 확인할 수 있다

비동기비동기

  • 비동기로 맡기는 코드가 또 비동기이다

    // callRequestRecommendation(_ movieId: Int) : 추천 영화 정보 api 호출 함수
    // -> 비동기
    
    func dispatchGroupNotify() {
        let group = DispatchGroup()
    
        DispatchQueue.global().async(group: group) {
            self.callRequestRecommendation(872585) { value in
                self.list0 = value
                print("0")
            }
        }
    
        DispatchQueue.global().async(group: group) {
            self.callRequestRecommendation(976573) { value in
                self.list1 = value
                print("1")
            }
        }
    
        DispatchQueue.global().async(group: group) {
            self.callRequestRecommendation(76600) { value in
                self.list2 = value
                print("2")
            }
        }
    
        DispatchQueue.global().async(group: group) {
            self.callRequestRecommendation(346698) { value in
                self.list3 = value
                print("3")
            }
        }
    
        group.notify(queue.main) {
            print("hi")
            self.posterCollectionView.reloadData()
        }
    }
    
  • group.notify가 가장 먼저 실행된다.
    따라서 reloadData()가 실행되어도 화면에 보여줄 데이터가 없다

  • group.notify가 실행되는 시점은 group의 비동기 코드들이 모두 일을 끝냈을 때 이다
  • 지금 위 코드의 비동기 코드들 입장에서 일을 끝냈다라는 건,
    데이터를 받아오는 것 이 아니라, 비동기로 일을 던지는 것이 된다
  • 즉, 얘네들 입장에서 데이터를 받아오는 건 상관이 없고
    일을 비동기로 던지는 것을 끝냈기 때문에 group.notify가 실행된다
  • 데이터를 받아오는 건 위 코드와 상관없이 알아서 실행된다

group enter / leave

  • group.enter() : RC += 1

  • group.leave() : RC -= 1

  • group에 대한 RC가 0이 되는 순간 notify가 실행된다

    func dispatchGroupEnter() {
    
        let group = DispatchGroup()
    
        group.enter()
        self.callRequestRecommendation(872585) { value in
            self.list0 = value
            print("0")
            group.leave()
        }
    
        group.enter()
        self.callRequestRecommendation(976573) { value in
            self.list1 = value
            print("1")
            group.leave()
        }
    
        group.enter()
        self.callRequestRecommendation(76600) { value in
            self.list2 = value
            print("2")
            group.leave()
        }
    
        group.enter()
        self.callRequestRecommendation(346698) { value in
            self.list3 = value
            print("3")
            group.leave()
        }
    
        group.notify(queue: .main) {
            print("END")
            self.posterCollectionView.reloadData()
        }
    }
  • 함수 실행 전, group의 RC를 올리고, 함수가 실행되면 RC를 내린다


Codable

  • 데이터를 디코딩하는 과정 : String -> Data -> Struct

Example1 (struct property name == json key value)

let json = """
{
	"quote": "The will of man is his happiness",
	"author": "Friedrich Schiller",
	"category": "happiness"
}
"""

// Struct
struct Quote: Decodable {  // 프로토콜 채택. 바깥에서 오는 데이터를 처리해줄게~ 라는 뜻
	let quote: String
	let author: String
	let category: String
}

// String -> Data
guard let result = json.data(using: .utf8) else { fatalError("error") }

// Data -> Struct
do {
	let value = try JSONDecoder().decode(Quote.self, from: result)
	print(value)
} catch {
	print(error)
}
  • 프로퍼티에 알맞게 잘 들어간다


Example2 (struct property name != json key value) by Optional

let json = """
{
	"quote": "The will of man is his happiness",
	"author": "Friedrich Schiller",
	"category": "happiness"
}
"""

// Struct
struct Quote: Decodable {
	let quoteContent: String?
	let author: String?
	let category: String?
}

// String -> Data
guard let result = json.data(using: .utf8) else { fatalError("error") }

// Data -> Struct
do {
	let value = try JSONDecoder().decode(Quote.self, from: result)
	print(value)
} catch {
	print(error)
}
  • 키 값에 맞지 않는 경우를 대비해 구조체 프로퍼티의 타입을 옵셔널로 선언한다
  • 안맞으면 nil로 저장된다


Example3 (From Snake To Camel)

let json = """
{
  "quote_content": "The will of man is his happiness.",
  "author_name": "Friedrich Schiller",
}
"""

// Struct
struct Quote: Decodable {   
    let quoteContent: String
    let authorName: String
}

// String -> Data
guard let result = json.data(using: .utf8) else { fatalError("error") }

// Data -> Struct
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
	let value = try decoder.decode(Quote.self, from: result)
	print(value)
} catch {
	print(error)
}
  • 프롬 뱀 투 낙타

Example4 (struct property name != json key value) by CodingKeys

let json = """
{
  "quote_content": "The will of man is his happiness.",
  "author_name": "Friedrich Schiller",
  "likelike": 34567
}
"""

// Struct - CodingKeys
struct Quote: Decodable {  
    let content: String
    let authorName: String
    let like: Int
   
    // 이걸 사용하지 않으면 JSON key와 변수명이 동일해야 한다
	// 정해져 있는 틀이기 때문에 스펠링 조심해야 함
    enum CodingKeys: String, CodingKey {    
        case content = "quote_content"
        case authorName = "author_name"
        case like = "likelike"
    }
}


// String -> Data
guard let result = json.data(using: .utf8) else { fatalError("error") }

// Data -> Struct
let decoder = JSONDecoder()
do {
	let value = try decoder.decode(Quote.self, from: result)
	print(value)
} catch {
	print(error)
}
  • enum CodingKeys를 써줘서 프로퍼티와 키를 맞춰준다
  • 만약 이걸 안쓴다면, 둘이 같아야 한다

Example5 (Accepting Null)

let json = """
{
  "quote_content": "The will of man is his happiness.",
  "author_name": null,
  "likelike": 34567
}
"""

// Struct
struct Quote: Decodable {
	let content: String
	let authorName: String
	let like: Int
	let isInfluencer: Bool  // 좋아요 3만개 이상이면 true

	enum CodingKeys: String, CodingKey {
		case content = "quote_content"
		case authorName = "author_name"
		case like = "likelike"
	}

	init(from decoder: Decoder) throws {
		let container = try decoder.container(keydBy: CodingKeys.self)
	
		content = try container.decode(String.self, forKey: .content)
		authorName = (try? container.decodeIfPresent(String.self, forKey: .authorName)) ?? "unknown"
		like = try container.decode(Int.self, forKey: .like)
		isInfluencer = (30000...).contains(like) ? true : false
	}
}

// String -> Data
guard let result = json.data(using: .utf8) else { fatalError("error") }

// Data -> Struct
let decoder = JSONDecoder()
do {
	let value = try decoder.decode(Quote.self, from: result)
	print(value)
} catch {
	print(error)
}
  • null값을 가질 때, 디폴트 값을 지정해둔다

QuickType

  • Example 1 ~ 5까지 다 할 줄 알면
  • 결론 : quicktype 쓰자

네트워크 코드 (SwiftyJSON vs. Codable)

SwiftyJSON

var movieList: [Movie] = []		// 이거 Movie 구조체도 직접 만들어야 해

func callReqeust(_ query: String) {
	let txt = ~~
    let url = ~~
    
    AF.request(url, method: .get)
    	.validate(statusCode: 200...500)
        .responseJSON { response in
        	switch response.result {
            case .success(let value):
            	let json = JSON(value)
                print(json)
                
                let statusCode = response.response?.statusCode ?? 500
                
                if (statusCode == 200) {
                    let title = json[""][""][""] 
                    // 이러면서 위치 찾고~
                    // 배열에다 append하고~
                }
            
            case .failure(let error)
            	print(error)
            }
        }
}

Codable

var result: BoxOffice? 		// 퀵타입이 만들어준 구조체😆

func callRequest(_ query: String) {
	let txt = ~~
    let url = ~~
    
    AF.request(url, method: .get)
    	.validate(statusCode: 200...500)
        .reponseDecodable(of: BoxOffice.self) { response in
        	
            let statusCode = response.response?.statusCode ?? 500
            
            if (statusCode == 200) {
            	guard let value = response.value else { return }
                self.result = value
                // 배열이 있어도 append 할 필요 없이
                // 그냥 통으로 넣어버려
                
                //completionHandler(value)
            }
            else {
            	print("Error! statusCode : \(statusCode)")
                print(response)
            }
        }
}

1개의 댓글

comment-user-thumbnail
2023년 8월 19일

즐겁게 읽었습니다. 유용한 정보 감사합니다.

답글 달기