기존에 Swift로 만들고자 기획했던 앱을 한번도 시도해보지 않았던 UIKit 프로젝트로 구현해보기 위해서 우선 UIkit에 대한 이해를 하고자 인프런 강의를 신청하였다.
청년수당을 통해 지급되는 지원금으로 강의를 수강할 수 있음에 감사한다.
[Passing Data]
1. Instance Property
- 가장 간단하게 이해할 수 있는게 해당되는 뷰컨트롤러나 클래스에 데이터를 접근해서 데이터를 넘겨주는 것은 대상의 클래스가 가지고 있는 인스턴스 프로퍼티에 데이터를 넘겨주는 것이다.
class ViewController : UIViewContorller {
var Data : String? // 이것을 인스턴프 프로퍼티라고 부른다.
}
/*
일반적으로 var, let을 변수, 상수라고 부르지만 class 내에 있는 즉, 클래스가 가지고 있는 변수이기 때문에 프로퍼티 라고 부른다.
*/
- MainViewController.swift와 연결된 AView.storyboard에서 버튼을 누르면 DetailViewController.swift와 연결된 DetailViewController.xib에 전달된 데이터가 함께 보여지도록 구성한다.
- 데이터를 받는 쪽 View가 보여지기 전에 받는쪽에서 가지고 있는 프로퍼티에다가 직접 값을 넣은 후 View를 실행함 **
1) MainViewController.swift
import UIKit
class MainViewController : UIViewContorller {
override func viewDidLoad() {
super.viewDidLoad()
}
// IBAction은 연결된 View에서 사용하는 기능이라는 것을 의미함
// 따라서 해당 기능은 뷰에서 버튼을 누를 때 사용되고, 버튼을 누르면 자동으로 detailVC가 실행됨
@IBAction func moveToDetail(_ sender: Any) {
// 해당되는 DetailViewController 클래스를 인스턴트화 시키는데, xib 기반으로 만들어져 있기 때문에 nibName에다가 DetailViewController.xib의 DetailViewController를 입력해준다.
let detailVC = DetailViewController(nibName: "DetailViewController", bundle: nil)
// someString은 DetailViewController.swift 내에 정의된 인스턴스
// someString이 기본값 "값 없음"에서 "보내는 데이터"로 변경된 후 View Presnet가 실행되게 된다.
detailVC.someString = "보내는 데이터"
// UIViewContorller이 가지고 있는 present 기능을 사용, 따라서 self를 붙여준다.
// 인스턴스화 된 DetailViewController뷰를 보여주기 위해서 사용됨
self.present(detailVC, animated: true, completion : nil)
}
}
2) DetailViewController.swift
import UIKit
class DetailViewController : UIViewContorller {
// 전달할 데이터
var someString = "값 없음"
// IBOutlet 연결된 View에서 사용하는 기능이라는 것을 의미함
@IBOutlet weak var someLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// 해당 함수는 화면이 로드될 준비가 완료된 경우 실행된다.
// 따라서 이 곳에서 데이터를 할당해주어야 한다.
// 값을 받아온 것이 없으면 위해서 "값 없음"이 들어가고, 데이터를 받아서 someString이 변경되면 받은 데이터가 넣어진다.
// 뷰에서 보이는 Label에 데이터가 들어감
someLabel.text = someString
}
}
** 여기서 중요한 점 **
- 현재 데이터를 받는 DetailViewController.swift 인스턴스 중 @IBOutlet으로 생성된 변수는 뷰가 실행될 때 메모리에 올라가야만 보여지는(=사용가능한) 변수로 뷰가 실행되기 전까지 메모리에 올라가 있지 않는다. @IBOutlet 변수가 메모리에 올라가는 시점은 MainViewController.swift에서 present를 실행하여 DetailViewController.swift에서 ViewDidLoad(), ViewWillAppear() 등이 실행되는 때이다. 일반 인스턴스는 그 앞줄에 DetailVC 인스턴스와 시에 메모리에 적재되어 사용이 가능하다.
- 따라서 MainViewController.swift에서 DetailVC.someLabel.text로 바로 접근이 불가능하고 DetailVC.someString을 통해서 가지고 있다가 DetailViewController.swift의 ViewDidLoad() 내부에서 someLabel.text = someString으로 할당을 해야하는 이유이다.
- 혹은 꼭 직접 MainViewController.swift에서 DetailVC.someLabel.text로 할당을 하고자 하는 경우 self.present(detailVC, animated: true, completion : nil)실행 아래 단락에서 명령어를 실행하면 되는데, 코딩 순서에 따라서 오류여부가 결정되기 때문에 이러한 방식은 사용을 주의해야 한다.
2. segue
- 여러개의 뷰컨트롤러가 하나의 StroyBoard에 있을 때 주로 많이 사용한다.
- MainViewController.swift와 연결된 AView.storyboard에서 버튼을 누르면 같은 StoryBoardt상에서 DetailViewController.swift와 연결된 BView.storyboard에 전달된 데이터가 함께 보여지도록 구성한다.
1) UI상에서 처리
- BView.storyboard의 기능을 수행해줄 DetailViewController.swift를 생성해주고 BView.storyboard의 Custom Class 메뉴에서 DetailViewController.swift를 선택하여 연결해준다.
- AView.storyboard에서 만든 "데이터 보내기 버튼"을 코드가 아닌 StoryBoard상에서 마우스 끌어오기로 BView.storyboard 빈 부분로 이동하여 Show기능을 선택하면 두 StoryBoard 사이에 화살표가 생성된다.
- 해당 화살표를 클릭하면 해당 StoryBoard Segue에 대한 이름(Identifier)을 설정해준다. 여기서는 segueDataPassing이라고 이름을 지어주었다.
- 이러면 기존에 self.present()를 통해서 뷰를 전화해주었던 부분을 화살표를 통해서 처리해 준 것이 된다.
2) MainViewController.swift
import UIKit
class MainViewController : UIViewContorller {
override func viewDidLoad() {
super.viewDidLoad()
}
// func prepare(for segue: )라는 함수는 화살표를 통해서 작업이 작업이 수행될 때 함께 실행되는 역할을 한다.
// 따라서 해당 함수 내부에서 화살표를 통해서 DetailViewController로 화면이 전활될 때 데이터를 넘겨주는 내용을 작성하면 된다.
override func prepare(for segue: UIStoryvoardSegue, sender: Any?) {
// segue에 존재하는 기본 기능인 identifier를 통해서 해당 이름이 맞는지 확인
// 원하는 segue인 경우 기본 기능인 destination을 통해서 해당 뷰와 연결된 ViewContoller를 실행함
if segue.identifier == "segueDataPassing" {
// segue.destination을 실행해서 연결된 ViewController가 DetailViewController로 타입캐스팅이 되는 경우 아래 구문 실행
if let detailVC = segue.destination as ? DetailViewController {
// 타입이 맞기 때문에 해당 ViewController의 인스턴스 접근 가능
detailVC.someString = "데이터 전달"
}
}
}
}
3) DetailViewController.swift
import UIKit
class DetailViewController : UIViewContorller {
var someString = "값 없음"
// IBOutlet 연결된 View에서 사용하는 기능이라는 것을 의미함
@IBOutlet weak var someLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
someLabel.text = someString
}
}
** 여기도 1번과 같은 문제점이 있음 **
- func prepare() 안의 if segue.identifier == "segueDataPassing" {} 내부에서 직접 @IBOutlet으로 되어있는 someLabel에 detailVC.someLabel.text로 접근을 해 버리면, 메모리 적제 순서에 따라 접근이 불가능해져 오류가 발생한다.
3. instance
- MainViewController.swift이 가진 인스턴스트 포인터를 DetailViewController.swift로 넘겨주는 개념 = MainViewController.swift에 대한 접근을 할 수 있도록 해주는 것
- MainViewController.swift와 연결된 AView.storyboard에서 버튼을 누르면 DetailViewController.swift와 연결된 DetailViewController.xib가 연결되고 DetailViewController.xib에서 데이터 전송 버튼을 누르면 AView.storyboard에 데이터를 보여주도록 구성한다.
1) MainViewController.swift
import UIKit
class MainViewController : UIViewContorller {
// DetailViewController.swift로 부터 받아온 데이터를 출력해주는 인스턴스
@IBOutlet weak var mainDataLabel : UILable!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func moveToInstance(_ sender: Any) {
let detailVC = DetailViewController(nibName: "DetailViewController", bundel: nil)
//DetailViewController.swift에 있는 인스턴스인 mainVC에다 MainViewController 자신을 넘겨주기 위해서는 self를 사용해서 넘겨준다.
detailVC.mainVC = self
self.present(detailVC, animated: true, completion: nil)
}
}
2) DetailViewController.swift
import UIKit
class DetailViewController : UIViewController {
var someString : String = "값 없음"
var mainVC : MainViewController?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func sendDataToMainVC(_ sender: Any) {
mainVC?.mainDataLabel.test = "데이터 전달"
self.dismiss(animated: true, completion: nil)
}
}
4. delegate(delegation) pattern
- MainViewController.swift와 연결된 AView.storyboard에서 버튼을 누르면 DetailViewController.swift와 연결된 DetailViewController.xib가 연결되고 DetailViewController.xib에서 데이터 전송 버튼을 누르면 AView.storyboard에 데이터를 보여주도록 구성한다.
- delegate는 데이터를 보내는 쪽, 받는쪽에서 모두 설정이 필요하다.
- delegate pattern은 보통 프로토콜을 준수하는 것들만 사용할 수 있도록 만든다.
1) MainViewController.swift
import UIKit
class MainViewController : UIViewContorller {
// DetailViewController.swift로 부터 받아온 데이터를 출력해주는 인스턴스
@IBOutlet weak var mainDataLabel : UILable!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func moveWithDelegate(_ sender: Any) {
let detailVC = DetailViewController(nibName: "DetailViewController", bundel: nil)
// 여기서도 DetailViewController.swift의 delegate에 MainViewController 자체를 넘겨준다.
// 따라서 MainViewController을 delegate의 타입인 DetailViewControllerDelegate을 준수하도록 만들어 줘야한다. -> 따라서 아래쪽에서 MainViewController을 extension해줌
// ** 3번과는 다르게 해당부에서 self는 MainViewController 전체가 아닌, delegate와 타입이 일치하는 extension 하위에 있는 내용만 접근하도록 연결된다.
detailVC.delegate = self
self.present(detailVC, animated: true, comletion: nil)
}
}
extension MainViewController : DetailViewControllerDelegate {
func passString(string: String) {
self.mainDataLabel.text = string
}
}
2) DetailViewController.swift
import UIKit
// 프로토콜 명은 일반적으로 사용할 VC이름 + Delegate를 붙여서 사용함
// 해당 프로토콜이 인스턴스화 되는 부분에서 weak 타입으로 정의되기 위해서는 해당 프로토콜이 AnyObject 타입으로 선언되어야 한다.
protocol DetailViewControllerDelegate : AnyObject {
// 데이터를 넘겨주는 역할을 대신하는 기능
// 프로토콜은 정의만 있고, 해당하는 내용(=바디)는 만들 수 없다.
func passString(string: String)
}
class DetailViewController : UIViewController {
// 해당 프로토콜의 정의가 사용되는 부분에서 이뤄지고 사용이 끝나면 해당 바디가 없어지도록 weak로 선언해줌
// delegate는 인스턴트가 없어도 접근할 수 있는 대리자를 만들어주는 역할이다.
weak var delegate : DetailViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func sendDataToMainVC(_ sender: Any) {
//실제 delegate의 passString()을 실행해서 데이터를 넘겨주는 부분
delegate?.passString(string: "delegate 데이터 전달")
self.dismiss(animated: true, completion: nil)
}
}
5. closure
- delegate처럼 호출부와 작동부가 따로 나눠지는 것처럼, closure도 나눠져 있다.
1) MainViewController.swift
import UIKit
class MainViewController : UIViewContorller {
// DetailViewController.swift로 부터 받아온 데이터를 출력해주는 인스턴스
@IBOutlet weak var mainDataLabel : UILable!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func moveWithClosure(_ sender: Any) {
let detailVC = DetailViewController(nibName: "DetailViewController", bundel: nil)
// DetailViewController.swift 인스턴스인 myClosure 클로저의 구현부를 아래 부분에서 만들어서 할당해준다.
// 파라미터는 String으로 str로 받아준다.
// 실제 호출은 DetailViewController.swift에서 메세지 호출 버튼을 누르면 하위 코드에서 해당 클로저를 실행할 때 사용된다.
detailVC.myClosure = { str in
mainDataLabel.text = str
}
self.present(detailVC, animated: true, completion: nil)
}
}
2) DetailViewController.swift
import UIKit
class DetailViewController : UIViewController {
// closure를 선언해줌
var myClosure : ((String) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func sendDataToMainVC(_ sender: Any) {
//함수 호출하는 것과 똑같이 ()과 매개변수 내용을 사용해서 호출해주면 됨
myClosure?("closure 데이터 전달")
self.dismiss(animated: true, completion: nil)
}
}
6. notification
- 데이터 연결을 위해서 연결점(인스턴스)을 만들어 줘야 하는데 notification은 상관없는 클래스와 데이터 전달하거나, 함수를 호출하기 위해 사용할 수 있다.
1) MainViewController.swift
import UIKit
class MainViewController : UIViewContorller {
// DetailViewController.swift로 부터 받아온 데이터를 출력해주는 인스턴스
@IBOutlet weak var mainDataLabel : UILable!
override func viewDidLoad() {
super.viewDidLoad()
// 확인할 Notification 이름을 정의해 준다.
let notificationName = Notification.Name("sendData")
// NotificationCenter는 OS에서 기본적으로 앱에서 감지하도록 하는 기능이다.
// selector는 Notification 정보를 받아오는 역할을 하는 함수를 넣어준다.
NotificationCenter.default.addObserver(self, selector: #selector(showData), name: notificationName, object: nil)
}
// 위에 Notification에서 Observer가 호출될 때 실행될 함수, 따라서 매개변수로 Notification정보를 받아올 수 있도록 정의해 줌
// objective 런타임 내 실행되어야 하기 때문에 @objc를 설정해줌
@objc func showData(notification: Notification) {
// Notification로 선언된 notification는 기본적으로 key:value 형식의 userInfo가 들어있다.
// 기본적으로 userInfo의 value는 Any타입으로 되어 있기 때문에 if let으로 원하는 타입으로 타입캐스팅이 되는지 확인해준다.
if let str = notification.userInfo?["str"] as? String {
self.mainDataLabel.text = str
}
}
@IBAction func moveWithNoti(_ sender: Any) {
let detailVC = DetailViewController(nibName: "DetailViewController", bundel: nil)
self.present(detailVC, animated: true, completion: nil)
}
}
2) DetailViewController.swift
import UIKit
class DetailViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func sendDataToMainVC(_ sender: Any) {\
let notificationName = Notification.Name("sendData")
let sendString = ["str" : "noti 데이터 전달"]
// post는 원하는 notification을 이름을 통해서 호출하는 역할을 한다.
// userInfo를 통해서 데이터를 넘겨준다.
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: sendString)
self.dismiss(animated: true, completion: nil)
}
}
addObserver를 여러번 호출하게 되면 selection에서 지정된 함수가 호출한 만큼 실행된다. 같은 내용으로 여러번 호출되지 않도록 주의해야 한다.
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification , object: nil) @objc func keyboardWillShow() { // 원하는 실행 내용 }
위와 같이 UIResponder를 통해서 기본적으로 내장된 키보드가 나타나는 OS적으로 자동화된 기능 사이에 실행되는 때에 호출해서 사용할 수도 있다.