// FirstProtocol.swift
protocol FirstSendDataProtocol {
func receiveData(_ sender: String)
}
// FirstView.swift
class FirstView: BaseView {
var delegate: FirstSendDataProtocol?
lazy var collectionView = {
let view = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout())
view.register(PracticeCollectionViewCell.self, forCellWithReuseIdentifier: "PracticeCollectionViewCell")
// First
view.delegate = self;
view.dataSource = self;
return view
}()
deinit {
print("First 뷰 deinit")
}
}
extension FirstView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.receiveData("hi \(indexPath)")
}
}
// FirstViewController.swift
class FirstViewController: BaseViewController {
let mainView = FirstView()
override func loadView() {
self.view = mainView
mainView.delegate = self
}
deinit {
print("First 뷰컨 deinit")
}
}
extension FirstViewController: FirstSendDataProtocol {
func receiveData(_ sender: String) {
print(sender)
navigationController?.popViewController(animated: true)
}
}
First 화면에 들어갔다가 나오면, 정상적으로 deinit() 함수가 실행되어야 하는데, 함수가 실행되지 않는다.
delegate 변수가 First VC의 RC를 올려주고 있기 때문에,
Stack 상에서 First VC의 RC를 낮춰도, 여전히 RC가 남아있게 된다
즉, Memory Leak이 발생한다
delegate가 First VC를 가리키긴 하지만, RC를 올리지 않게 하기 위해 weak 키워드를 사용한다.
weak은 거의 클래스에서만 사용하기 때문에, delegate 프로토콜에 AnyObject를 써주어서 클래스만 해당 프로토콜을 채택할 수 있게 한다
// FirstProtocol.swift
protocol FirstSendDataProtocol: AnyObject {
func receiveData(_ sender: String)
}
// FirstView.swift
weak var delegate: FirstSendDataProtocol?
View에서는 객체 생성 / 뷰에 등록(addSubview) / 레이아웃 만 설정해준다
네트워크 통신을 통해 받은 데이터들을 바로 cell에 적용해줄 수 있다
// SecondViewController.swift
class SecondViewController: BaseViewController {
let mainView = SecondView()
override func loadView() {
self.view = mainView
// Second
mainView.collectionView.dataSource = self
mainView.collectionView.delegate = self
}
}
extension SecondViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 바로 화면 전환이 가능하다
navigationController?.popViewController(animated: true)
}
}
네트워크 요청에 대한 정보를 표현하는 객체
URL 객체를 통해 통신하거나,
URLRequest 객체를 통해 세부 옵션(캐싱, HTTP Method 등)을 선택할 수 있다
// URLRequest
init(
url: URL,
cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy,
timeoutInterval: TimeInterval = 60.0
)
class APIManagerClosure {
static let shared = APIService()
private init() { }
func callRequest(completionHandler: @escaping (Photo?) -> Void) {
// 에러가 났을 때 completionHandler(nil)로 실행시키기 때문에
// Photo? 로 선언한다
let url = URL(string: ~~)
var request = URLRequest(url: url!, timeoutInterval: 10)
URLSession.shared.dataTask(with: request) { data, response, error in
/* ===========DispatchQueue.main.async ===========*/
// 1. 에러 체크
if let error {
completionHandler(nil)
return
}
// 2. response 체크
guard let response = response as? HTTPURLResponse,
(200...500).contains(reseponse.statusCode) else {
completionHandler(nil)
return
}
// 3. data 체크
guard let data = data else {
completionHandler(nil)
return
}
do {
let result = try JSONDecoder().decode(Photo.self, from: data)
completionHandler(result)
print("SUCCESS. RESULT : ", result)
}
catch {
print("ERROR : ", error)
completionHandler(nil)
}
}.resume()
}
}
// SecondViewController.swift
class SecondViewController: BaseViewController {
var list: Photo = Photo(total: 0, total_page: 0, result: [])
override func viewDidLoad() {
super.viewDidLoad()
APIMangerClosure.shared.callRequest { photo in
// photo에 nil 들어올 가능성
guard let photo = photo else {
// Error Alert을 띄워준다
return
}
print("API END")
/* ===========DispatchQueue.main.async ===========*/
self.mainView.collectionView.reloadData()
}
}
}
DispatchQueue.main.async
로 해당 코드를 감싸주어야 한다// SecondViewController.swift
extension SecondViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PracticeCollectionViewCell", for: indexPath) as? PracticeCollectionViewCell else { return UICollectionViewCell() }
cell.backgroundColor = .blue
// 이미지 다운로드
let thumb = list.results[indexPath.item].urls.thumb // 이미지 링크
let url = URL(string: thumb) // url 변환
DispatchQueue.global().async {
// Data() 코드가 동기로 실행되기 때문에 비동기도 돌려준다
let data = try! Data(contentsOf: url!)
// UI 작업은 다시 main으로 돌려준다
DispatchQueue.main.async {
cell.imageView.imate = UIImage(data: data)
}
}
return cell
}
}
class LoadingViewController: UIViewController {
// 세션 선언
var session: URLSession!
// 몇 % 다운받았는지 확인하기 위한 요소
// 1. 총량
var total: Double = 0
// 2. 현재 다운받은 양
var buffer: Data? {
didSet {
let result = Double(buffer?.count ?? 0) / total
// total이 0이면 divided by zero 때문에 NaN이 출력됨
if total != 0 {
progressLabel.text = "\(result * 100)%)"
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// 버퍼 초기화 (초기화하지 않으면 append가 되지 않는다)
buffer = Data()
let url = URL(string: "https://apod.nasa.gov/apod/image/2308/M66_JwstTomlinson_3521.jpg")
// 1. 세션 환경설정
session = URLSession(
configuration: .defualt,
delegate: self,
delegateQueue: .main
}
// 2. 데이터 (Task)
session.dataTask(with: url!).resume()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// 리소스 정리. 모두 무효화하고 취소
session.invalidateAndCancel()
// 진행중인 것 까지만 다운로드하고, 중지
session.finishTasksAndInvalidate()
}
}
extension LoadingViewController: URLSessionDataDelegate {
// 서버에서 최초로 응답
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) async -> URLSession.ResponseDisposition {
print("RESPONSE: ", response)
if let response = response as? HTTPURLResponse,
(200...500).contains(response.statusCode) {
total = Double(response.value(forHTTPHeaderField: "Content-Length")!)!
return .allow
}
else {
return .cancel
}
}
// 서버에서 데이터 받을 때마다 호출
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
print("DATA : ", data)
buffer?.append(data) // 누적해서 buffer에 추가
}
// 서버에서 응답 완료된 후 호출
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("END")
if let error {
print(error)
}
else {
guard let buffer = buffer else {
print(error)
return
}
// 세션 환경설정에서 main으로 설정해주었기 때문에 DispatchQueue.main으로 돌릴 필요 없다
imageView.image = UIImage(data: buffer)
}
}
}
protocol PassDataDelegate {
func receiveData(data: String)
}
var delegate: PassDataDelegate?
override func viewDidDisappear(_ animated: Bool) { // 화면이 종료되는 시점에 전달
super.viewDidDisappear(animated)
if let txt = mainView.textField.text {
if (!txt.isEmpty) {
delegate?.receiveData(data: txt)
}
}
}
extension AViewController: PassDataDelegate {
func receiveData(data: String) {
mainView.nameButton.setTitle(data, for: .normal)
}
}
@objc
func buttonClicked() {
let vc = BViewController()
vc.delegate = self
navigationController?.pushViewController(vc, animated: true)
}
var completionHandler: ( (String) -> Void )?
override func viewDidDisappear(_ animated: Bool) { // 화면이 종료되는 시점에 전달
super.viewDidDisappear(animated)
if let txt = mainView.textField.text {
if (!txt.isEmpty) {
completionHandler(txt)
}
}
}
@objc
func buttonClicked() {
let vc = BViewController()
vc.completionHandler = { str in
self.mainView.userNameButton.setTitle(str, for: .normal)
}
navigationController?.pushViewController(vc, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(newIntroduction),
name: NSNotification.Name("introduction"),
object: nil
)
}
@objc
func newIntroduction(notification: NSNotification) {
if let intro = notification.userInfl?["new intro"] as? String [
mainView.introButton.setTitle(intro, for: .normal)
}
}
override cunf viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if let txt = mainView.textView.text {
if (!txt.isEmpty) {
NotificationCenter.default.post(
name: NSNotification.Name(rawValue: "introduction"),
object: nil,
userInfo: ["new intro": txt]
)
}
}
}
@objc
func introButtonClicked() {
print("hi intro")
/* 정바향 값전달 테스트 */
NotificationCenter.default.post(
name: NSNotification.Name(rawValue: "sendDataForward"),
object: nil,
userInfo: ["forward Data": "this is forward"]
)
let vc = IntroViewController()
navigationController?.pushViewController(vc, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
/* 정방향 값전달 테스트 */
NotificationCenter.default.addObserver(
self,
selector: #selector(printData),
name: NSNotification.Name("sendDataForward"),
object: nil
)
}
@objc
func printData(notification: NSNotification) {
print("hi")
if let data = notification.userInfo?["forward Data"] as? String {
print(data)
} else {
print("NotificationCenter 정방향 값전달 실패. data = nil")
}
}
addObserver
addObserver
보다 post
가 먼저 신호를 보내면 addObserver
가 신호를 받지 못한다addObserver를 실행하는 위치가 중요하다.
viewDidLoad에서 실행시키면 단 한 번만 실행되기 때문에 중복될 위험이 없지만,
화면을 넘어가는 버튼이나 그 외 여러 번 실행되는 곳에 addObserver를 실행시키면 여러 번 화면 전환이 일어날 때, 중복해서 addObserver가 실행되는 문제가 발생한다
@objc
func introButtonClicked() {
print("hi intro")
NotificationCenter.default.addObserver(self, selector: #selector(newIntroduction), name: NSNotification.Name("introduction"), object: nil)
let vc = IntroViewController()
navigationController?.pushViewController(vc, animated: true)
}
@objc
func newIntroduction(notification: NSNotification) {
print("add Observer 실행")
if let intro = notification.userInfo?["new intro"] as? String {
mainView.introButton.setTitle(intro, for: .normal)
}
}
// 모든 옵저버 제거
NotificationCenter.default.removeObserver(self)
// 특정 옵저버 제거
NotificationCenter.default.removeObserver(
self,
name: "introduction,
object: nil
)
NotificationCenter.default.removeObserver(
self,
name: "forward data",
object: nil
)
lazy와 weak 키워드를 함께 사용하면 컴파일 오류가 발생한다