[SeSAC] August 29, 2023

✔︎ 오늘의 정리

  • 접근 제어자
    • 모듈과 타겟
    • 각 접근 제어자가 언제 쓰이는지와 어떻게 쓰이는지!!
  • 값 전달
    • closure
    • delegate
  • UIPickerView
  • PHPicker

 


 

접근 제어자

소스파일 및 모듈에서 세부적인 코드의 내용에 대한 접근을 막거나 허용할 때 사용한다. 이를 통해 코드에 대한 불필요한 노출을 제한함으로써 은닉화를 구현할 수 있다. 접근 제어자는 모듈과 타겟의 특성으로 나뉘게 되는데, 먼저 그 둘에 대해서 알아보자면 다음과 같다.

  • 모듈
    • 우리가 흔히 말하는 프로젝트 단위를 모듈이라고 한다. 프레임워크도, import를 통해 사용하는 라이브러리도 하나하나가 모듈이다!
    • open / public을 통해 접근할 수 있당
  • 타겟
    • 하나의 프로젝트 파일 내에서 코드를 제어할 경우 타겟이라고 한다.
    • 근데 타겟 왠지 익숙하지 않나?!

 

 

맞다! 이 친구 이름도 타겟이당. 하지만 이 친구는 모듈과 타겟에서 말하는 타겟은 아니고~~!!! 오히려 모듈에 속한다.

그야 프로젝트 인포에 들어가는 타겟들은 라이브러리나 아니면 다른 기능이어서 다른 프로젝트로 모듈화된 친구들이니까~~~!!!!

걍 이름만 같은 것일뿐 혼동하지말장

 

  • 접근 제어자의 종류 및 특징
    • open / public
      • open이나 public으로 선언된 외부 코드들은 모두 내 프로젝트에서 사용 가능하다. import만 한다면!!
      • 둘의 차이는 class에서 사용 가능하냐 아니냐의 차이로, open의 경우 class에서 사용 가능하며 상속과 오버라이딩이 모두 가능하다! 반대로 public의 경우에는 모두 불가능하다고 보면 된당
    • interval / fileprivate / private
      • 같은 모듈 내에서 접근이 가능한 경우를 말한다. 다만, 그 범위에서 차이가 있다!
      • interval의 경우에는 접근 제어자를 사용하지 않았을 경우 default로 사용되며, 같은 모듈 내에서 얼마든지 접근이 가능하고 상속도 받을 수 있다.
      • fileprivate의 경우에는 말 그대로 해당 파일 내에서만 접근이 가능하다!
      • private은 범위가 더 좁아져서 해당 블록 내에서만 접근이 가능하다.

 

 

값 전달

값을 전달하는 방법에는 크게!! 네 가지 방법이 있다.

  • property
  • notificationCenter
  • closure
  • delegate

 

그 중 notificationCenter를 이용해서 하는 건 따로 글을 쓰구 있어서 클로저와 딜리게이트를 활용해서 값을 전달하는 법을 쓰도록 하겟다...

오늘 내내 이걸 연습해서 TIL에는 이게 더 맞는 것 같기도 허다

 

 

클로저로 값 전달하기

  • 전달하기 전 뷰 컨트롤러에서 데이터를 입력하는 뷰 컨트롤러를 열 때 클로저를 전달해 주면 된다.
  • 그러려면? 데이터를 입력하는 뷰 컨트롤러에 클로저 변수가 있어야 할 것이다.
  • 해당 클로저 변수는 전달하려는 값을 매개변수로 가지고, 데이터를 입력하는 뷰 컨트롤러가 사라질 때나 특정 버튼이 눌릴 때 전달을 원하는 값을 매개변수로 클로저에 전달하여 다른 뷰 컨트롤러에서!! 클로저가 뒤늦게 실행된다.
  • 보통 이 클로저 안에는 전달하기 전 뷰 컨트롤러의 뷰를 변경하는 코드가 들어가 있다!! 값을 전달한 걸 뷰에 보여줘야 하니까!

 

자세한 코드는 다음과 같다.

import UIKit
import PhotosUI

class ProfileViewController: BaseViewController {
    let mainView = ProfileView()
    
    var settingList = SettingList().user
    
    override func loadView() {
        self.view = mainView
    }
    
    override func configureView() {
        super.configureView()
        mainView.tableView.delegate = self
        mainView.tableView.dataSource = self
        
        title = "Profile"
        
        mainView.imageButton.addTarget(self, action: #selector(profileClicked), for: .touchUpInside)
        mainView.profileButton.addTarget(self, action: #selector(profileClicked), for: .touchUpInside)
    }
    
    ... ...
}

extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return settingList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: ProfileTableViewCell.identifier) as? ProfileTableViewCell else { return UITableViewCell()}
        let settings = settingList[indexPath.row].name.rawValue
        let data = settingList[indexPath.row].user
        cell.settingLabel.text = settings
        cell.textField.text = data
        cell.textField.placeholder = settings
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 55
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let setting = settingList[indexPath.row].name
        if setting == .Gender {
            let vc = PickerViewController()
            vc.setting = setting
            // 클로저로 값 전달!!
            vc.completionHandler = { text in
                self.settingList[indexPath.row].user = text
                tableView.reloadRows(at: [indexPath], with: .none)
            }
            navigationController?.pushViewController(vc, animated: true)
            tableView.reloadRows(at: [indexPath], with: .automatic)
        } else {
            let vc = EditingViewController()
            vc.setting = setting
            // 여기두!!
            vc.completionHandler = { text in
                self.settingList[indexPath.row].user = text
                tableView.reloadRows(at: [indexPath], with: .none)
            }
            navigationController?.pushViewController(vc, animated: true)
            tableView.reloadRows(at: [indexPath], with: .automatic)
        }
    }
    
    
}
//
//  EditingViewController.swift
//  TMDB Project
//
//  Created by yeoni on 2023/08/29.
//

import UIKit

class EditingViewController: BaseViewController {
    
    let textField = {
        let view = UITextField()
        return view
    }()
    
    //  아까 ProfileViewController에서 클로저를 넣어 주면
    //  EditingViewController에서 클로저가 뒤늦게 실행되면서 
    // 다시!! ProfileViewController에 값을 넣어주게 댐 ^_^
    var completionHandler: ((String) -> ())?
    
    var setting: SettingName?
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // 저장해서 값 넘겨주는 코드!!
        
        guard let text = textField.text else { return }
        completionHandler?(text)
        
    }
    
    override func configureView() {
        super.configureView()
        view.addSubview(textField)
        
        guard let setting else { return }
        let str = setting.rawValue
        
        title = str
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "저장", style: .plain, target: self, action: #selector(saveButtonClicked))
        
        switch setting {
        case .Name, .Username, .Bio:
            textField.placeholder = "Enter your \(str)"
        case .Pronouns, .Link:
            textField.placeholder = "Enter the \(str)"
        case .Gender:
            return
        }
        
    }
    
    @objc func saveButtonClicked() {
        // 저장해서 값 넘겨주는 코드!!
        guard let text = textField.text else { return }
        
        completionHandler?(text)
        navigationController?.popViewController(animated: true)
    }
    
    override func setConstraints() {
        textField.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).inset(20)
            make.horizontalEdges.equalTo(view.safeAreaLayoutGuide).inset(16)
            make.height.equalTo(50)
        }
    }
}

넘 .... 대충 그렸나 싶은데 아무튼 이해한 걸 바탕으로 그린 건 다음과 같다. 

 

 

 

 

 

 

 

delegate로 값 전달하기

조금 더 명확히 하자면 프로토콜과 딜리게이트를 이용해서 값 전달하기? 가 맞는 것 같다. ^_^

  • protocol을 정의해 준다. 
    • 이 프로토콜은 내가 받고자 하는 값을 매개변수로 가지고 있어야 한다! 그래야 프로토콜을 이용해서 가지고 올 수 있당.
protocol PassTextDelegate {
    func receiveText(text: String)
}
  • 이후, 데이터를 전달받고자 하는 뷰에서 해당 프로토콜을 채택한 후, 전달받을 값을 이용하여 뷰를 어떻게 그릴지 함수를 맹글어 주면 된다.
  • 데이터를 전달하고자 하는 뷰에서는 해당 프로토콜 타입의 옵셔널 변수를 하나 생성한 뒤에, 전달할 값이 있는 곳에 해당 변수, 그러니까 프로토콜의 함수를 호출해 주면 된다. 내가 넣고 싶은 값을 넣어서!
  • 이제 값을 전달할 준비는 끝났으니 받기만 하면 된다! 데이터를 받고자 하는 뷰에서 데이터를 주려는 뷰로 화면 전환을 할 때, 아까 만들었던 옵셔널 변수에 스스로를 넣어준다. 그러니까, 옵셔널 변수를 호출하게 된다면 실행은 자기가 하겠다는 것이다.
  • 그럼 옵셔널 변수 안의 함수가 실행될 때, 전달을 원하는 값을 담은 함수가 데이터를 받고자 하는 뷰에서 실행되게 되는 것이다.

ㅋㅋ 사실 첨에는 진짜 이해가 안 돼서 손코딩도 했다...

그러니까 어디서 보내고? 어떻게 굴러가는지가 그려지드라

 

 

 

내가 이해한 건 다음과 같다. 

 

자세한 예제는 아래에!

import UIKit
import PhotosUI

class ProfileViewController: BaseViewController {
    let mainView = ProfileView()
    
    var settingList = SettingList().user
    
    override func loadView() {
        self.view = mainView
    }
    
    override func configureView() {
        super.configureView()
        mainView.tableView.delegate = self
        mainView.tableView.dataSource = self
        
        title = "Profile"
        
        mainView.imageButton.addTarget(self, action: #selector(profileClicked), for: .touchUpInside)
        mainView.profileButton.addTarget(self, action: #selector(profileClicked), for: .touchUpInside)
    }
    
    @objc func profileClicked() {
        var configuration = PHPickerConfiguration()
        configuration.selectionLimit = 1
        configuration.filter = .images
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        self.present(picker, animated: true, completion: nil)
    }
}

... ...

extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return settingList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: ProfileTableViewCell.identifier) as? ProfileTableViewCell else { return UITableViewCell()}
        let settings = settingList[indexPath.row].name.rawValue
        let data = settingList[indexPath.row].user
        cell.settingLabel.text = settings
        cell.textField.text = data
        cell.textField.placeholder = settings
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 50
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let setting = settingList[indexPath.row].name
        if setting == .Gender {
            let vc = PickerViewController()
            // 화면 전환하려는 뷰의 딜리게이트 변수를 자기로 설정해 주어 자기가 함수를 실행하겠다는 뜻이다
            vc.delegate = self
            navigationController?.pushViewController(vc, animated: true)
            tableView.reloadRows(at: [indexPath], with: .automatic)
        } else {
            let vc = EditingViewController()
            vc.setting = setting
            vc.completionHandler = { text in
                self.settingList[indexPath.row].user = text
                tableView.reloadRows(at: [indexPath], with: .none)
            }
            navigationController?.pushViewController(vc, animated: true)
            tableView.reloadRows(at: [indexPath], with: .automatic)
        }
    }
    
    
}

extension ProfileViewController: PassTextDelegate {
	// 함수 정의하깅
    func receiveText(text: String) {
        for index in settingList.indices {
            let setting = settingList[index]
            if setting.name == .Gender {
                settingList[index].user = text
                print(settingList)
                mainView.tableView.reloadData()
                return
            }
        }
    }
}

 

//
//  PickerViewController.swift
//  TMDB Project
//
//  Created by yeoni on 2023/08/30.
//

import UIKit

class PickerViewController: BaseViewController {
    
    let list = ["성별을 선택해 주세요", "Male", "Female", "They / Them"]
    
    let pickerView = {
        let picker = UIPickerView()
        return picker
    }()
    
    // pickerViewController로 화면이 전환될 때 이전 뷰컨과 연결되어 있당
    var delegate: PassTextDelegate?
    
    override func configureView() {
        super.configureView()
        
        view.addSubview(pickerView)
        
        title = "Gender"
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "저장", style: .plain, target: self, action: #selector(saveButtonClicked))
        
        pickerView.delegate = self
        pickerView.dataSource = self
    }
    
    @objc func saveButtonClicked() {
        navigationController?.popViewController(animated: true)
    }
    
    override func setConstraints() {
        pickerView.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).inset(10)
            make.horizontalEdges.equalTo(view.safeAreaLayoutGuide)
        }
    }
    

}


extension PickerViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    
    // 몇 개를 선택할 건지!!
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    // 선택할 수 있는 pickerView의 갯수
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return list.count
    }
    
    // title을 정해주깅
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return "\(list[row])"
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        if row == 0 {
            giveAlert(title: "성별을 골라 주세요!", message: "")
            delegate?.receiveText(text: "")
        } else {
        	// 이 뷰컨에서는 따로 정의한 receiveText가 없는데 어케 처리하지?
            // => 정답! 아까 화면 전환 하기 전 뷰컨트롤러랑 연결해 줬으니까
            // 화면 전환 하기 전 뷰컨트롤러에서 정의한 함수대로 실행된다!
            // 물론 실행도 해당 뷰컨트롤러에서 되고!
            delegate?.receiveText(text: "\(list[row])")
        }
    }
    
}

 

 

 

 

 

 

 

 

 

 

 

 

 

UIPickerView

	let pickerView = UIPickerView()

이런 식으로 선언하여 불러준 후,

 

아래 extension에서 pickerViewDelegate와 pickerViewDataSource를 따로 호출해 준다!!

 

extension PickerViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    
    // 몇 개를 선택할 건지!!
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    // 선택할 수 있는 pickerView의 갯수
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return list.count
    }
    
    // title을 정해주깅
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return "\(list[row])"
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // 얘는 선택한 값을 delegate를 이용하여 넘겨준 것 ^_^ 
        delegate?.receiveText(text: "\(list[row])")
    }
    
}

 

 

이후 viewDidLoad에서 delegate / dataSource를 연결해 주면 끝!! ^_^

 

근데 맨날 연결해 주는 거 까묵고 왜 픽커뷰가 안 되지? 하고 잇다

보통 pickerView가 안 뜨면 delegate / dataSource를 연결 안 해 준 거드라...

코드를 수정하면서 세 번 정도 다시 썼는데 세 번 다 delegate / dataSource를 호출 안 해서 아니 왜 안 떠??? 하다가 찾고 허망해졌다

컴퓨터는 바부라서 말을 안 들으면 내가 일 잘못 시킨 건데 이젠 걍 컴퓨터가 한번쯤 틀려줬음 좋겟다는 마음으로 하고 있다

이게맞나?!

암튼...

 

전에 썼던 친구인데 올만에 쓰니까 메서드가 하나도 기억이 안 나드라

다시찾아봣다...

한번에 두 개 복습하기 짱~

 

 

 

 

 

 

 

 

 

 

 

 

 

PHPicker

수업 시간에는 UIPickerViewController를 써봤던 걸루 기억하는데 PHPicker도 궁금해서 먼저 써봤다

먼저!! UIPickerViewController와 다른 점은 권한 설정을 묻지 않아도 댄다는 것이다.

또, 여러 개를 고르는 걸 자체적으로 지원하고 video / image / livePhoto까지 모두 지원댄다~~!!!!

내부의 method를 통해서 사용자가 몇 개까지 고르게 할지, 지원하는 asset들 중에 어떤 걸 사용자가 올릴 수 있게 할지를 설정할 수 있당.

 

자세한 코드는 아래와 같다!!

 

import UIKit
import PhotosUI

class ProfileViewController: BaseViewController {
    let mainView = ProfileView()
    
    var settingList = SettingList().user
    
    override func loadView() {
        self.view = mainView
    }
    
    override func configureView() {
        super.configureView()
        mainView.tableView.delegate = self
        mainView.tableView.dataSource = self
        
        title = "Profile"
        
        mainView.imageButton.addTarget(self, action: #selector(profileClicked), for: .touchUpInside)
        mainView.profileButton.addTarget(self, action: #selector(profileClicked), for: .touchUpInside)
    }
    
    @objc func profileClicked() {
    
    	// picker 기본 설정!!
        var configuration = PHPickerConfiguration()
        
        // 최대 몇 개까지 고르게 할지!!
        configuration.selectionLimit = 1
        
        // 어떤 거만 허용할지!
        configuration.filter = .images
        
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        self.present(picker, animated: true, completion: nil)
    }
}


extension ProfileViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        
        // 이미지 클릭 시 화면 dismiss
        picker.dismiss(animated: true)
        
        // itemProvider == 선택한 asset을 보여주는 역할을 함!!
        if let itemProvider = results.first?.itemProvider,
           itemProvider.canLoadObject(ofClass: UIImage.self) {
            let type: NSItemProviderReading.Type = UIImage.self
            itemProvider.loadObject(ofClass: type) { image, error in
                if let image = image as? UIImage {
                	// View를 다시 그려주는 거기 땜에 main에 넣어주깅...
                    DispatchQueue.main.async {
                        self.mainView.profileImageView.image = image
                        
                    }
                } else {
                    print(error)
                }
            }
        }
    }
}

 

 

 

사용할 때 'UIImage must confirm to _ObjectiveCBridgable'라는 오류가 나서 애먹었는데 ㅠ_ㅠ 다행히도 stackoverflow에서 해결했다.

 

자세한 글은 요기!

 

 

https://stackoverflow.com/questions/66240835/uiimage-must-conform-to-objectivecbridgeable-in-phpickerviewcontrollerdelegate

 

UIImage must conform to _ObjectiveCBridgeable in PHPickerViewControllerDelegate callback

I have reviewed the WWDC2020 video explaining how to adopt the new PHPickerViewController API. I have also seen several blogs showing exactly the following code. func picker(_ picker:

stackoverflow.com

 

 

 

내가 이해한 바로는, loadObject()는 두 가지 구현이 있는데, 내가 원하는 함수가 아닌 다른 함수를 컴파일러가 불러온 모양이다. 따라서, 타입을 명확히 해 주어 컴파일러에게 옳은 함수를 불러올 수 있도록 힌트를 주는 과정이

    let type: NSItemProviderReading.Type = UIImage.self

    result.itemProvider.loadObject(ofClass: type) { image, error in
        ...
    }

요 과정인 것 같다. ^_^ b

바부바부컴파일러

 

 

 

 

 

 

 

 

아힘들다

근데너무잼밋다

ㅋㅋ

쩨기랄...

어케일어나지

얼렁자자...

 

 

'TIL' 카테고리의 다른 글

[SeSAC] September 4, 2023  (2) 2023.09.07
[SeSAC] September 1, 2023  (0) 2023.09.02
[SeSAC] August 28, 2023  (0) 2023.08.29
[SeSAC] August 27, 2023  (1) 2023.08.28
[SeSAC] August 21, 2023  (2) 2023.08.22