어제 BaseViewController를 만들어서 중복되는 코드나 통일성있는 코드를 만드는 방법을 공부했는데 구글링을 더하다보니 BaseViewController에 generic을 이용해서 viewModel을 주입시키는 방법이 있어서 프로젝트를 만들어봤다
// MARK: - 기존의 baseViewController에서 viewModel또한 baseViewModel이 존재한다는 가정하에 baseViewModel를 상속받은 ViewModel
class ViewController<T>: UIViewController {
let viewModel: T
init(viewModel: T) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
setUI()
setConstraints()
}
func setUI() {}
func setConstraints() {}
}
우선 원래 BaseViewControlle였는데 이름을 ViewController로 바꾸고 class에서 generic을 사용하면 클래스를 선언할때 특정 타입을 명시해줘야한다
// MARK: - BaseViewController<T>는 ViewController를 상속하고 BindVIewModelProtocol을 채택한 타입이다
// 이때 T는 generic이고 ViewController가 class이기때문에 class에서 generic은 사용할때 T에 타입을 명시를 해줘야하고 해당타입을 viewController의 T로 인식한다
typealias BaseViewController<T> = ViewController<T> & BindViewModelProtocol where T: BaseViewModel
// MARK: - viewModel관련해서 꼭 구현해야할 함수의 경우 여기서 구현해주면된다
protocol BindViewModelProtocol {
func bindViewModel()
}
그리고 typealias로 방금만든 viewController와 ViewModel을 사용할때 꼭 구현해야하는 함수를 protocol로 만들어서 함께 채택할 수 있게 선언을 해줬다. 이때 T는 BaseViewModel이라는 프로토콜을 채택한타입이어야한다고 선언해줬고
protocol BaseViewModel {
// MARK: - value 에는 어떤 타입일지 모르기 때문에 associatedtype을 사용합니다.
associatedtype T
func fetchData()
func getDate() -> [T]
func addData(_ data: T)
func deleteData(index: Int)
}
BaseViewModel은 이런 프로토콜이다. generic으로 associatedtype을 사용한 이유는 모든 ViewModel은 데이터를 fetch해오고 get해오고 add하고 delete하는 함수를 구현해야하는데 저기에 들어가는 모델은 어떤 모델일지 모르니까 associatedtype을 선언해놓으면 실제로 BaseViewModel을 각자의 ViewModel에서 채택하면
import UIKit
class MainViewModel: BaseViewModel {
typealias T = Model
private var models = [Model]()
func fetchData() {
models = [
.init(name: "김의성", age: 27),
.init(name: "기믜성", age: 26)
]
}
func getDate() -> [Model] {
return models
}
func addData(_ data: Model) {
models.append(data)
}
func deleteData(index: Int) {
models.remove(at: index)
}
}
이런식으로 내가만든구조체 Model을 가지고 데이터 처리를 할 수 있는 함수로 만들 수 있다
import UIKit
class MainViewController: BaseViewController<MainViewModel> {
func bindViewModel() {
viewModel.fetchData()
}
private var testLabel: UILabel = UILabel().then {
$0.text = "testLabel"
$0.font = .systemFont(ofSize: 15)
$0.tintColor = .blue
$0.textAlignment = .center
}
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
override func setUI() {
view.addSubview(testLabel)
}
override func setConstraints() {
testLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(100)
}
}
}
MainViewControlle에서는 처음에 정의한 typealias를 사용하면 ViewController를 상속함과 동시에 BindViewModelProtocol을 채택한 상태가 된다.
그리고 T는 MainViewModel(이건 BaseViewModel이라는 프로토콜을 채택하고있으니까 가능)선언해줬다
데이터를 전달하는 방법에는 여러가지가 있겠지만 대표적으로는 delegate패턴을 사용한다던지 closure를 사용한다던지 프로퍼티에 접근해서 데이터를 전달하는 방법 이 3가지로 프로젝트를 진행했었는데
"NotoficationCenter"라는 방식도 데이터를 전달하는 방법이라고 해서 한번 사용해봤다
우선 처음에는 이렇게 생각을 했었다
첫번째 VC에서 신호를 보내고 navigation으로 두번째 VC로 넘어가서 신호를 받아서 label에 text를 변경시켜보려헀는데 이게 아무리 해도 안되길래 구글링을 열심히 해봤다 그랬더니 이게 VC가 모두 메모리에 올라가있는 상황에서만 NotificationCenter를 사용할수있다고 한다 내가 생각했던대로 메모리에 올라가있지 않은데 신호를 받을 수 없다고 한다
그래서 두번쨰 VC에서 버튼을 누르면 기존 VC로 pop해서 textfield값을 전달해주는 방식으로 구성해봤다
두번째 VC에서 버튼을 누르면 아래함수가 실행된다
@objc func testButtonTapped() {
guard let text = textfieldView.text, !text.isEmpty else {
print("글자를 입력해주세요")
return
}
NotificationCenter.default.post(name: .testButtonTapped, object: text)
navigationController?.popViewController(animated: true)
}
여기서 중요한 부분이
NotificationCenter.default.post(name: .testButtonTapped, object: text)
이부분이다 NotificationCenter의 개념은 어딘가에서 신호를 보내면 해당 신호를 받는 observer들이 신호를 받아서 특정 action을 취해주는거다 즉 여기서 .testButtonTapped라는 이름을가진 신호를 보내준다(text를 담아서)
그러면 어딘가에 설치되어있는 observer가(이 observer는 .testButtonTapped에만 반응하는녀석) 특정 action을 하면된다 당연히 observer은 첫번째 VC에 있고
NotificationCenter.default.addObserver(self, selector: #selector(recieveTestButtonNotification(_:)), name: .testButtonTapped, object: nil)
이렇게 생겼다 name같은경우엔 구글링을 해보니 보통
extension Notification.Name {
static let testButtonTapped = Notification.Name("test")
}
이런식으로 사용하는거같다 어쨋든 여기서 observer가 .testButtonTapped신호를 감지하게되면
recieveTestButtonNotification(_:)함수를 실행하게 되는데 이건 UIButton의 addtarget과 비슷하다
@objc func recieveTestButtonNotification(_ notification: Notification) {
guard let text = notification.object as? String else {
return
}
testLabel.text = text
}
이런식으로 정의해주면 된다
기본적으로 우리가 case가 정해져있는 부분을 개발할때 enum을 많이쓴다
그래서 그 enum타입을 가지고 switch문을 통해서 분기처리를 많이하는데 이때 쓰는 attribute키워드가 두가지 있는데 default에 사용하는 @unknown과 enum을 선언할때 사용하는 @frozen이다
enum을 switch문으로 분기처리 할때 보통은 모든 case에 대한 처리를 해줘야한다
이때 enum에 새로운 case를 추가하게되면 해당 enum을 사용하는 모든부분에서 error가 발생하게된다
enum Alphabet {
case a
case b
case c
}
func printAlphtbet(_ alphabet: Alphabet) {
switch alphabet {
case .a:
print("a")
case .b:
print("b")
case .c:
print("c")
}
}
예를들어 위와같은 enum이있고 각각의 case를 print하는 함수가있다고할때
case d를 추가하면 무슨일이 발생하냐
이렇게 케이스를 추가하라고 나온다. 그러면 각각 케이스를 추가하면된다. 하지만 이게 싫으면 default를 선언하면되는데
func printAlphtbet(_ alphabet: Alphabet) {
switch alphabet {
case .a:
print("a")
case .b:
print("b")
case .c:
print("c")
default :
print("others")
}
}
이게 문제가 case가 추가되었는데 case가 중요해서 모든 분기처리를 해줘야하는데 default를 해놓으면 추가된 모든 case가 default로 흡수(?)되어버린다 warning도 error도 발생하지 않기 때문에 문제가생길가능성이 높아진다
이럴떄 default앞에 @unknown키워드를 사용해주는것이다
그러면 이렇게 error가 발생하지않고 경고창이 뜨는데 여기서도 case가 missing되었다고 알려주고 case를 추가할 수 있게 해준다. 그래서 이렇게 해두면 코드의 안정성이 좋아진다
@frozen은 enum에서 case가 변하지않는게 확실할때 예를들어 Result타입같은 경우에 case가 성공과 실패밖에없는것처럼 이런경우에
@frozen
enum Alphabet {
case a
case b
case c
}
이렇게 선언해주면 case가 바뀔일이 없다고 선언해줌과동시에 효율적인 컴파일이 가능해진다고 한다
ios 13이전에는 app delegate가 app의 lifecycle과 ui의 lifecycle을 모두 담당했다고 한다
-> 지금이야 프로젝트를 만들면 appdelegate파일과 secnedelegate파일이 만들어지지만 예전에는 appdelegate파일만 만들어졌다고 한다
하지만 multiwindow 기능이 추가되면서 이런 방식의 변화가 필요하게 된다
원래는 하나의 window만 보여주면 되었기 때문에
appdelegate가 app도 키고 window(우리가 흔히 말하는 ui)도 켜고하면 된거같은데
이게 여러개의 window를 띄워야하기시작하면서 이 각각의 window은 당연히 자신만의 생명주기가 필요하므로 이를 잘 컨트롤하기 위해 AppDelegate에 더해 SceneDelegate가 추가할수밖에 없어졌다
여러개의 인스턴스를가질수있게되는게 multi window기능이라고 생각하면 편할듯 싶다
애플 공식문서를 보면
Scene은 여러 Window를 포함할 수 있는 하나의 인스턴스라고 정의할 수 있게된다
->정확한지는 모르겠지만 scene은 아이폰의 화면, window는 앱의 ui라고 생각하면 될듯?
->scene하나가 여러개의 window를 가질 수 있다 -> 멀티윈도우기능?
즉 원래는 하나의 window만 관리하면됬었는데 이게 mulitwindow기능이 추가되면서 window를 두개 세개를 사용할수있게 하기위해서 이 window들을 포함할수있는 인스턴스의 개념인 scene이 등장하게 된다
하나의 아이폰 화면에 하나의 window만 뜨면 아이폰화면과 앱화면을 딱히 구분할 이유는 없음
-> 아이폰의 화면에 여러개의 window가 뜰수있게 되면서 scene개념이 필요하게됨
ios13이후의 앱실행과정
1. appdelegate의 didFinishLaunchingWithOptions를 실행해서 앱을 실행할준비가되었다는걸 알림(true로)
2. 나 이런 이름의 scene session을 보내고싶어!라는 내용을 UISceneConfiguration객체를 통해 알려준다 -> info.plist에서 찾아서 그걸 보냄
3. appdelegate와 scenedelegate가 UIsceneSession으로 서로 의사소통을 한다.(scene session을 주고 받는다)
4. Scene delegate에서 받은 scene session를 연결하고 window(화면에 띄울 ui)를 구성하고 화면에 띄워준다