✔︎ 오늘의 정리
- 접근 제어자
- 모듈과 타겟
- 각 접근 제어자가 언제 쓰이는지와 어떻게 쓰이는지!!
- 값 전달
- closure
- delegate
- UIPickerView
- PHPicker
접근 제어자
소스파일 및 모듈에서 세부적인 코드의 내용에 대한 접근을 막거나 허용할 때 사용한다. 이를 통해 코드에 대한 불필요한 노출을 제한함으로써 은닉화를 구현할 수 있다. 접근 제어자는 모듈과 타겟의 특성으로 나뉘게 되는데, 먼저 그 둘에 대해서 알아보자면 다음과 같다.
- 모듈
- 우리가 흔히 말하는 프로젝트 단위를 모듈이라고 한다. 프레임워크도, import를 통해 사용하는 라이브러리도 하나하나가 모듈이다!
- open / public을 통해 접근할 수 있당
- 타겟
- 하나의 프로젝트 파일 내에서 코드를 제어할 경우 타겟이라고 한다.
- 근데 타겟 왠지 익숙하지 않나?!
맞다! 이 친구 이름도 타겟이당. 하지만 이 친구는 모듈과 타겟에서 말하는 타겟은 아니고~~!!! 오히려 모듈에 속한다.
그야 프로젝트 인포에 들어가는 타겟들은 라이브러리나 아니면 다른 기능이어서 다른 프로젝트로 모듈화된 친구들이니까~~~!!!!
걍 이름만 같은 것일뿐 혼동하지말장
- 접근 제어자의 종류 및 특징
- open / public
- open이나 public으로 선언된 외부 코드들은 모두 내 프로젝트에서 사용 가능하다. import만 한다면!!
- 둘의 차이는 class에서 사용 가능하냐 아니냐의 차이로, open의 경우 class에서 사용 가능하며 상속과 오버라이딩이 모두 가능하다! 반대로 public의 경우에는 모두 불가능하다고 보면 된당
- interval / fileprivate / private
- 같은 모듈 내에서 접근이 가능한 경우를 말한다. 다만, 그 범위에서 차이가 있다!
- interval의 경우에는 접근 제어자를 사용하지 않았을 경우 default로 사용되며, 같은 모듈 내에서 얼마든지 접근이 가능하고 상속도 받을 수 있다.
- fileprivate의 경우에는 말 그대로 해당 파일 내에서만 접근이 가능하다!
- private은 범위가 더 좁아져서 해당 블록 내에서만 접근이 가능하다.
- open / public
값 전달
값을 전달하는 방법에는 크게!! 네 가지 방법이 있다.
- 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에서 해결했다.
자세한 글은 요기!
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 |