32YB SOPT/iOS 세미나

[SOPT] iOS_2주차_정규세미나 (데이터 전달 심화, SnapKit을 통한 AutoLayout, InjectIII, Then)

신_이나 2023. 4. 8. 11:59

 

 

 

목차

1. 데이터 전달 심화

- Protocol / Delegation을 활용한 데이터 전달

- Notification Center를 활용한 데이터 전달

- Closure를 활용한 데이터 전달

- Segue prepare 메소드를 활용한 데이터 전달


2. AutoLayout

- Frame-Based Layout

- Auto-Layout


3. InjectionIII, SnapKit, Then

- SPM (Swift Package Manager)

- InjectionIII

- SnapKit

- Then


4. 실습 코드

- Protocol / Delegation 을 활용한 데이터 전달 실습

- Notification Center 를 활용한 데이터 전달 실습

 

 

23.04.12 SOPT iOS 2주차 정규세미나 내용

예습을 안한 관계로 세미나 내용 + 정리까지 한 번에 하도록 하자!

 

 

 

1. 데이터 전달 심화

데이터 전달에는 직접 전달 방식, 간접 전달 방식 이렇게 2가지가 있다. 그렇다면 두 가지는 무엇이 다를까?

 

직접 전달 방식은 '동기 방식'이라고 말하며, A에서 B로 데이터를 직접 넘겨주는 방식을 의미한다. 이 방식에는 오늘 배울 protocol/delegaion, notificationcenter 와 그 외 segue prepare, present/push, closure  방식 등이 있다.

 

간접 전달 방식은 '비동기 방식'이라고 말하며, A가 데이터를 다른 곳에 저장해두면, B,C,D가 어디서든 필요할 때 꺼내가는 방식을 의미한다. AppDelegate.swift, UserDefaults, CoreData, Realm, 싱글톤패턴 등을 활용할 수 있다. 이는 추후 세미나에서 다루거나 다루지 않을 예정이다.

 

직접 전달의 경우, 받는 뷰 컨트롤러에서 받아야할 데이터들의 명세를 미리 정의해주어야 하고, 간접 전달의 경우 저장소 위치를 공유하고 있어야 한다.

 

 

 

 

- Protocol / Delegation을 활용한 데이터 전달

 

Protocol 이란, 어떤 기능에 적합한 특정 메소드, 프로퍼티 및 기타 요구 사항의 청사진을 의미한다.(설계도같은 의미) 즉, 다른 주체들에게 특정한 틀(질문?)을 제공하면서 각 주체들이 틀의 결과(질문에 대한 답)을 받는 것이다. 사실 파트장님은 iOS, 서버가 각자 다른 대답을 했다고 예시 들어주셨는데 너무 쏙쏙 들어왔다. 

 

Delegation는 코드자체로 전환하는 것을 말한다. 따라서 대리자인 Delegation 을 선언하여 사용할 수 있다. 여기서 중요한 것이, 모든 일을 처리하는 것이 아니라 일부를 대리자에게 넘기는 것이다.(객체지향프로그램!) 기능을 위임할 수 있는 대리자, 즉 객체가 있다는 것은 직접 구현해야 하는 부분이 적기 때문에 효율적이다.

 

//우리가 만든 Box 의 프로토콜
//각각의 메소드가 표시되어 있다.
protocol BoxProtocol: AnyObject {

	func 메소드1()
    func 메소드2()
    func 메소드3()
    func 메소드4()
    
}

//Box라는 클래스 내부에 우리가 만든 BoxProtocol 형태의 값을 저장하는 delegate를 저장하였다.
class Box {
	
    weak var delegate: BoxProtocol?
    
    func 메소드1() {
     	delegate?.메소드1()
    }
}

//class A는 Box의 프로토콜을 채택하였다.
class A: BoxProtocol {

	//box 하나를 생성하는데 이 box는 Box에서 참조한 것이다.
    //왜 그랬을까? 왜 Box에서 참조했을까?????????
    //=> protocol은 말그대로 대리인이기에 틀만 제공할 뿐이다.
    //따라서 당연히 데이터 자체는 Box에서 제공받아야 한다
	var box: Box 
    
    private init(box:Box) {
    	self.box = box
        //Box에서 참조한 box의 대리인은 class A가 하겠다는 것
        //따라서 class A는 Box의 프로토콜로 설정한 함수를 참조하였다.
        //그렇기 때문에 protocol에서 기본 설정해준 틀대로 질문하고 답변(return)하게 된다
   		box.delegate = self
    }
    
    func 메소드1(){
    	~~~
    }
    
    func 메소드2(){
    	~~~
    }
    ~~~
}

 

위는 세미나에서 수업한 내용을 한꺼번에 적은 코드다.

데이터 저장소 Ena Company가 있을 때, 여기서 데이터를 가지고 가고 싶은 소비자가 있다고 치자. 이때 성장한 Ena Company는 많은 소비자 각각에게 해당하는 데이터를 매번 가져다주기 어렵기 때문에 Protocol 이라는 안내서를 제공하여 소비자가 직접 자기 데이터를 가져가도록 가이드라인을 제공하는 것이다. 착한 소비자들은 가이드라인에 맞춰 내가 뭘 가져가고 싶은지 체크하고 답변하면서 Ena Company의 데이터를 거쳐가는 통로를 마련하는 것이다.  내가 이해한 게 맞을까? 대충 맞다고 한다.

 

따라서 class A는 Box라는 회사의 protocol을 참조하였고, 그렇기 때문에 Box회사에서 제공한 protocol/delegate 에 맞게 자기가 답한 대로 그 답을 수행하겠다는 것이기에 box.delegate = self 로 사용할 수 있는 것이다!

 

 

 

// 프로토콜 선언 (어느 위치에 선언해도 상관 없음)

protocol sendDataDelegate: AnyObject {
	func sendData(data: String)
}

// ----- 전달할 화면 (다음 화면) 속에 해당 코드 구현 -----

// 1. 전달할 화면(secondVC)에 대리자 선언 ( 강한 순환 참조 방지를 위해 weak 변수 선언)

	weak var delegate: sendDataDelegate?
    
// 2. 특정 시점에 데이터 전달

	self.delegate?.sendData(data: "문자 보내기")

// ----- 원래 화면 (이전 화면) 속에 해당 코드 구현 -----


// 1. 원래 화면(이전 화면)에 sendDataDelegate 채택

class firstVC: UIViewController, sendDataDelegate {
	func sendData(data: String) {
    // 받은 데이터를 처리할 코드를 이곳에 구현
    }
}

// 2. 원래 화면 클래스 속 Button에 전달할 화면의 Delegate를 채택

@IBAction func Button(_ sender: UIButton) {
        
        guard let secondVC = self.storyboard?.instantiateViewController(withIdentifier: "secondVC") as? SecondVC else { return }
        secondVC.delegate = self
        self.present(secondVC, animated: true, completion: nil)
}

출처 : https://dusanbaek.tistory.com/23

 

Dev : Storyboard, Protocol 및 Delegate를 활용한 화면 간 데이터 전달

Dev : Storyboard를 활용한 화면 전환 4가지 방법 * push는 navigation controller가 연결된 상태에서만 작동 * 1. segue로 push 전환 : 원래 화면에 있는 Button을 끌어다가 이동할 VC에 'Show'로 연결 뒤로가기 : 이동

dusanbaek.tistory.com

 

위는 구글링에서 찾은 코드다. 위도 마찬가지로 delegate를 선언하여 protocol로 데이터를 받고 있다.

그럼 여기서 weak var 과 AnyObject 는 무엇일까?

 

weak 이란, 약한 참조를 말한다. 그렇다면 당연히 반대는 strong 강한 참조다.

weak 은 해당 인스턴스의 소유권을 가지지 않고 주소값만을 가지고 있는 포인터 개념이다. 따라서 인스턴스의 count를 증가시키지 않는다. 또한 release도 발생하지 않는다. 메모리가 해제될 경우엔 자동으로 nil로 초기화 된다. 두 개의 객체 변수의 메모리가 해제되었을 때 강한 참조 일때와 달리 약한 참조를 한 객체는 ARC에서 자동으로 객체의 메모리까지 해제를 시켜준다. 따라서 weak 속성을 사용하는 객체는 optional 타입이어야 하며 (nil일 수도 있기 때문), 값이 변할 수도 있기 때문에 var 로 받아야 한다.

delegate 에서 weak 을 쓰는 이유는, 메모리가 누수되는 문제를 막기 위해 사용된다. 만약에 메모리가 해제될 경우엔 자동으로 nil로 초기화되기 때문이다. 이는 retain cycle 을 피하기 위해서인데, retain cycle 란 메모리가 해제되지 않고 유지되어 메모리 누수가 발생하는 현상이다. class - instance - property 가 서로 연결되었을 때, instance 가 nil 이 된다면, property는 연결이 끊겨 쓸모없는 데이터가 된다. 

 

++그렇다면 weak 은 여기서 property 도 모두 nil 을 만들어 주는 걸까??????

파트장님 답변참고) var 자체가 strong 이기 때문에 사실상 strong 이라는 말을 따로 쓰지 않는다. 따라서 var만 선언한다면 모달을 내려도 (창(?)을 닫아도) 메모리가 계속 남아있다. 하지만 weak var 를 선언해주면 약한참조로 선언되는데, 이때는 모달을 내림과 동시에 메모리도 없어진다. 따라서 내가 질문한 모두 nil 을 만들어 주는 뭐 그런 것보단 ... weak 으로 선언된 것들의 메모리가 해제된다는 의미로 이해하면 될 것 같다. 지금은 간단한 데이터 전송이라 괜찮지만 나중가서 앱개발을 본격적으로 하게 되면 이 메모리 선언이 굉장히 중요하다!

 

 

 

AnyObject 란, 모든 클래스 타입의 인스턴스를 나타낼 수 있다. (Any 는 함수타입을 포함하여 모든 타입의 인스턴스를 나타낼 수 있다.) 또한 위 사진에서 말하는 바처럼, AnyObject 는 프로토콜이다. 근데 나 자꾸 프로토콜 할 때마다 프로토타입 생각나,, 쇼미더 머니,,,

따라서 계속 위 코드들에서 AnyObject를 참조하고 있었던 것이다.

 

 

 

 

Notification Center를 활용한 데이터 전달

 

NotificationCenter는 NotificationCenter에 등록된 event 가 발생하면 해당 event에 대한 행동을 취한다. NotificationCenter 자체는 정보를 저장하기 위한 구조체다. 따라서, event가 발생하면 해당 event 알림을 등록한 옵저버에게 NotificationCenter에 등록된 정보를 저장하는 것이다. 이때 NotificationCenter는 옵저버를 처리할 때까지 대기하기 때문에 흐름이 동기적으로 흘러간다. 1. 옵저버 등록 2. NotificationCenter에서 옵저버에게 호출로 이루어진다.

//name 은 전달하고자 하는 신호 이름
var name: Notification.Name

//object은 전달하고자 하는 데이터
//Any 타입이기 때문에 모든 인스턴스를 나타낼 수 있다.
var object: Any?

//userInfo는 신호와 관련된 값 또는 객체의 저장소
userInfo: [AnyHashable : Any]?

세미나 자료를 참고하여 NotificationCenter라는 구조체는 위와 구성되어 있다.

추가로 더 구체적으로 알아보자

 

// 노티피케이션 발송
 NotificationCenter.default.post(name: NSNotification.Name("TestNotification"), object: nil, userInfo: nil)
 
// 옵저버 등록
//addObserver -  관찰자 대기
//selector - 관찰자가 수행해야 할 업무를 의미
 NotificationCenter.default.addObserver(self, selector: #selector(didRecieveTestNotification(_:)), name: NSNotification.Name("TestNotification"), object: nil)

 @objc func didRecieveTestNotification(_ notification: Notification) {
         print("Test Notification")
 }

//Name 등록 방식
// 1)addObsever 등록시 같이 등록
// Post
NotificationCenter.default.post(name: Notification.Name("doItSomeThing"), object: nil)
// Add Observer
NotificationCenter.default.addObserver(self, selector: #selector(printSomeThing(_:)), name: Notification.Name("doItSomeThing"), object: nil)

// 2)Extension 으로 property 추가
extension Notification.Name {
    static let doItSomeThing = Notification.Name("doItSomeThing")
}

//Post 값 전달하기
NotificationCenter.default.post(name: .testnoti, object: "testobject")

@objc func getValue(_ notification: Notification){
	let getValue = notification.object as! String
	print(getValue)
}

참고 : https://silver-g-0114.tistory.com/106

 

[iOS/Swift] NotificationCenter 사용하기

NotificationCenter 란? NotificationCenter 에 등록된 event 가 발생하면 해당 event에 대한 행동을 취합니다. 앱 내에서 메세지를 던지면 아무데서나 이 메세지를 받을 수 있게 하는 역할을 합니다. 보통 백그

silver-g-0114.tistory.com

위에서는 전체적인 틀을 알아보았고, 자세한 사용은 아래 실습에서 실제로 구현한 코드를 보도록 하자!

 

 

 

 

- Closure를 활용한 데이터 전달

Closure 는 여기저기에서 나타나 날 혼란스럽게 한다. 

++ 세미나에서도 closure 과 관련된 데이터 전달을 쓰-윽 하고 지나가서 잘 이해하지 못하였다. 따라서 클로저는 클로저의 정의부터 데이터 전달까지 모두 한 번에 다루는 포스팅을 따로 작성하도록 하자

++ 추후 포스팅 주소

 

 

 

 

- Segue prepare 메소드를 활용한 데이터 전달 (세미나에서 언급 X)

 

Segue prepare 메소드를 활용한 데이터 전달은 직접 전달 방식에 해당한다. 세그란 스토리보드에서 뷰 컨트롤러에서 다른 뷰 컨트롤러로 화면 전환을 해주기 위해 코드를 사용하지 않고 구현할 수 있는 도구를 말한다. 세그에서 중요한 것은 출발점과 도착점이다. 따라서 화면 전환이 시작되는 곳이 출발점, 화면 전환이 완료되는 곳이 도착점이라고 할 수 있다. 

func prepare(
    for segue: UIStoryboardSegue,
    sender: Any?
)

위와 같이 사용하며 구체저으로는 viewController 에서 prepare 함수를 override 해준 뒤 메소드 안에 전달할 데이터를 작성하여 구현할 수 있다. 그 뒤엔 SecondViewController에 전달된 데이터가 label를 넣어주기 위하여 viewDidLoad 함수에 textLabel.text = text 를 작성한다. 이것은 1주차 세미나에서 작성한 방법과 동일하다.

 

 

 

 

2. AutoLayout

Auto-Layout 이란 말 그대로 자유롭게 레이아웃을 변경하는 것을 말한다. 앞선 1주차에도 언급한 바가 있다. 이처럼 Auto-Layout를 사용할 수 있는 방법은 여러가지가 있는데 그중에서도 SnapKit에 대하여 알아보자!

 

 

- Frame-Based Layout

수동으로 설정해주는 방식을 알면 AutoLayout의 유용성을 더욱 잘 느낄 수 있다. 수동으로 직접 좌표를 설정해주는 방식에는 Frame-Based Layout 이 있다.  Frame-Based Layout이란 말 그대로 좌표를 통하여 레이아웃을 구현하는 방법을 말한다. 디바이스 왼쪽 상단을 (0,0)으로 설정한 뒤, 그에서 오른쪽을 x축으로, 아래를 y축으로 잡는 것이다. 파트장님이 세미나에서 제시해주신 Frame-Based Layout의 코드 예시는 아래와 같다.

let firstView = UIView(frame: CGRect(x: 20, y: 20, width: 120, height: 80))

let secondView = UIView(frame: CGRect(x: 20, y: 108, width: 120, height: 80))

 

 

 

 

코드에서 첫 번째 뷰와 두번째 뷰의 크기와 위치를 정해주었는데, 첫번째 뷰는 좌표 (20,20)에서 시작하여 너비 120, 높이 80을 가지는 뷰를 의미하고, 두번째 뷰는 좌표 (20,108)에서 시작하여 너비 120, 높이 80을 가지는 뷰를 의미한다. 

 

여기서 CGRect 이란 사각형의 위치와 크기를 포함하는 구조체를 말한다. (rect이 사각형을 의미함) 그 밖에도 CGPoint, CGSize 가 있다. 먼저, CGPoint는 2차원 좌표계의 점을 포함하는 구조체다.(애플문서 참고) 의미 그대로 좌표만을 나타낸다고 생각하면 된다. CGSize는 너비와 높이를 포함하는 구조체다. 정말 단순히 너비와 높이만 포함한다. 따라서 이 두가지를 합친(?) 것이 바로 CGRect 라고 할 수 있다!

 

 

 

 

 

하지만 위와 같이 Frame-Based Layout을 사용하였을 경우에 핸드폰 기종이 달라지거나 화면을 회전하였을 경우에 화면이 잘리거나 안나오는 경우가 있을 수 있다. 따라서 이때 필요한 것이 Auto-Layout이다. 

 

 

 

 

- Auto-Layout

Auto Layout은  Constraint 의 모음이라고 말한다. 이때  Constraint 은 제약 조건을 이야기하며, 객체의 좌, 우 위, 아래의 여백에 대한 제약조건을 설정하는 것이다. 이  Constraint 는 1차 방정식 혹은 부등식의 형태를 띠며 유일한 해를 갖도록 나아가야 한다.

y = ax + b

Constraint 이 갖는 방정식은 위와 같은 형태다.

세미나에서 들었던 예시를 다시 살펴보자.

1. 
NSLayoutConstraint.activate([nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                             nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)])
  
2.
NSLayoutConstraint.activate([nextButton.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20),
                             nextButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30),
                             nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30),
                             nextButton.heightAnchor.constraint(equalToConstant: 48)])

1번 코드는 view에 맞추어 nameLabel 의 위치를 설정하였다. 따라서 nameLabel의 x의 값은 view의 수평 중심에 맞추고, y의 값은 view의 수직 중심에 맞추도록 설정한 코드다.

2번 코드는 nameLabel 에 맞추어 newButton을 설정하였다. newButton의 top을 nameLabel 의 bottom에서 20 떨어진만큼, newButton의 top을 nameLabel 의 bottom에서 20 떨어진만큼 ,,, 와 같이 설정해준 코드다.

 

근데 이때 nameLabel 은 x,y 좌표만 설정해준 반면, nextbutton은 x,y 좌표 뿐만 아니라 너비와 높이도 함께 다 지정하였다. 이 이유는 UILabel 이 Intrinsic Content Size을 가지기 때문이다. Intrinsic Content Size는 본래 갖추어진 사이즈를 의미하는데, font의 크기, text의 길이, numberOfLines에 따라 width, height등이 결정되어 나타나는 것이다. 하지만 지정하고 싶다면 따로 지정할 수 있다.

 

 

 

3. InjectionIII, SnapKit, Then

- SPM (Swift Package Manager)

SPM이란 종속성 관리를 위한 공식 도구로 First party Tool 이기 때문에 편리하게 사용할 수 있다. First party Tool 이란 Xcode에 내재된 기능이라고 생각할 수 있다.

 

- InjectionIII

코드가 변경될 때마다 바로바로 시뮬레이터에 반영되어 개발 시간을 줄일 수 있는 tool 이다. 

 

- SnapKit

SnapKit은 편리한 Auto Layout 코드 구현을 위한 라이브러리다. SPM으로 SnapKit 을 설치할 수 있다. 

//UI Component 생성 및 속성 정의
private let nameLabel: UILabel = {
        let label = UILabel()
        label.text = "이름이 무엇인가요!?"
        label.font = .systemFont(ofSize: 16)
        label.textColor = .blue
        label.textAlignment = .center
        return label
}()

//addSubView
[nameLabel, nameTextField, backButton].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview($0)
}

//Auto Layout 정의
NSLayoutConstraint.activate([nameLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 300),
                                     nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30),
                                     nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30)])

UI Component 를 정의와는 다르게 addSubView 와 Auto Layout 을 정의하는 것은 꼭 구현해 주어야 한다.

 

  • 기본 구조 makeConstraints 
NSLayoutConstraint.activate([nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                             nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)])

nameLabel.snp.makeConstraints {
	$0.center.equalToSuperView()
}

nameLabel.snp.makeConstraints { make in
  make.center.equalToSuperview()
}

makeConstraints는 제약조건을 만들어주는 과정이다. 위처럼 코드를 구현하여 기본 구조를 만들 수 있다. 원래는 코드로 레이아웃을 잡아줄 때, translatesAutoresizingMaskIntoConstraints = false를 작성해주어야 했다. 하지만 snapkit 을 사용할 때는 작성하지 않아도 된다.

 

  • equalTo(), equalToSuperView()equalTo() 란 말 그대로 ~에 동일하다 라는 의미다.
  • SuperViewsuperView 란 상위뷰를 의미한다.
  • equalToSuperView()SuperView()와 동일하게 취급함을 의미한다.
  • offset(), Inset()offset은 element 와의 간격에 사용하며, inset은 superView와의 간격에 사용한다. offset의 경우 현재뷰의 constraint = 슈퍼뷰의 constraint +offset 값 으로 식을 작성하며 bottom 과 right 는 마이너스 부호를 갖게 된다. Ex) $0.top.left.equalToSuperview().offset(50) or $0.bottom.right.equalToSuperview().offset(-50) inset의 경우엔 좌표값이 양수 값이다. 따라서 Ex. $0.top.left.bottom.right.equalToSuperview() .inset(UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50)) 으로 잡아줄 수 있으며 모든 값이 동일 할 때는 $0.edges.equalToSuperview().inset(50) 으로 간단하게 표기 가능하다!
  • centerX, centerY, centercenterX - 수평의 중심, centerY - 수직의 중심, center - 정가운데!
  • width, height, sizewidth 와 height 의 값이 동일하다면 아래와 같이 사용할 수 있다.
$0.size.equalTo(44) <==> $0.width.height.equalTo(44)

 

 

 

- Then

Then 라이브러리는 속성에 관한 코드를 깔끔하게 나타낼 수 있도록 도와주는 라이브러리다. Then 도 마찬가지로 SPM 으로 설치하면 된다. then 을 사용하면 $0을 사용하여 더 간단하게 작성할 수 있다. 이것도 세미나 자료를 다시 복습해보도록 하겠다. 

//수정 전 코드
private let nameLabel: UILabel = {
        let label = UILabel()
        label.text = "이름이 무엇인가요!?"
        label.font = .systemFont(ofSize: 16)
        label.textColor = .blue
        label.textAlignment = .center
        return label
}()

//수정 후 코드 1번
private let nameLabel = UILabel().then {
        $0.text = "이름이 무엇인가요!?" = 
        $0.font = .systemFont(ofSize: 16)
        $0.textColor = .blue
        $0.textAlignment = .center
}

//수정 후 코드 2번
private let nameLabel = UILabel()
func setStyle() { 
			nameLabel.do {
            $0.text = "이름이 무엇인가요!?"
            $0.font = .systemFont(ofSize: 16)
            $0.textColor = .blue
            $0.textAlignment = .center
            }
}

label 과 return 을 작성하지 않아도 되기 때문에 더 간단하게 작성할 수 있다.