-
Notifications
You must be signed in to change notification settings - Fork 0
[week03] MVC
xcode 시뮬레이터 추가
상단 메뉴에서 window
>Device and Simulators
클릭 후, 좌측 하단 + 버튼을 눌러 원하는 기종 추가한다.
UIButton을 통해 등록정보 입력하기
TextField
와 Label
을 @IBOutlet weak var
로 연결한다. 이때 접근제어자 private
으로 캡슐화를 해주면 좋다.
사용자 입력 정보를 받기 위해 UserInfo Struct
를 생성하여 Register Button
구동 시 textField
에 입력된 정보를 받아온다. 이때 공백을 없애기 위해 .trimmingCharacters(in: .whitespacesAndnewlines)
를 사용한다.
받아온 사용자 입력 정보를 Struct
안에 저장한 뒤 Check Button
구동 시 저장된 정보를 label
에 넣어 준다.
실험 코드 보기
struct UserInfo {
var name: String
var phoneNumber: String
}
class ViewController: UIViewController {
@IBOutlet weak private var nameTextField: UITextField!
@IBOutlet weak private var phoneNumberTextField: UITextField!
@IBOutlet weak private var nameLabel: UILabel!
@IBOutlet weak private var phoneNumberLabel: UILabel!
var registrantList: [UserInfo] = []
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func hitRegisterButton(_ sender: Any) {
let name = nameTextField
.text?
.trimmingCharacters(in: .whitespacesAndNewlines)
let phoneNumber = phoneNumberTextField
.text?
.trimmingCharacters(in: .whitespacesAndNewlines)
let registrant = UserInfo(name: name!, phoneNumber: phoneNumber!)
registrantList.append(registrant)
}
@IBAction func hitCheckButton(_ sender: Any) {
let lbName = registrantList.last?.name
let lbPhoneNumber = registrantList.last?.phoneNumber
nameLabel.text = lbName
phoneNumberLabel.text = lbPhoneNumber
}
}
ViewController.swift 코드보기
import UIKit
struct Registrant {
let name: String
let phoneNumber: String
}
class ViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var phoneNumberTextField: UITextField!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var phoneNumberLabel: UILabel!
var registrantList: [Registrant] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func hitRegisterButton(_ sender: Any) {
register()
}
@IBAction func hitCheckButton(_ sender: Any) {
let registrant = registrantList.last
nameLabel.text = registrant?.name
phoneNumberLabel.text = registrant?.phoneNumber
}
func register() {
let registrant = Registrant(
name: nameTextField.text!,
phoneNumber: phoneNumberTextField.text!
)
registrantList.append(registrant)
nameTextField.text = ""
phoneNumberTextField.text = ""
}
}
실험 2에서는 ViewController
의 예시 코드 안에서 View
와 Controller
를 구분해 보자고 이야기한다.
해당 코드에는
View
가 존재하지 않는다. 오직Controller
만 있다.
이렇게 생각하게 된 이유를 설명하기 위해 Main.storyboard
파일을 열어보자.
프로젝트에 작업해 둔 Scene
이 보일것이다.
이 상태에서 탭바의 우측을 보면, →
←
가 상하로 배치된 메뉴(Enable Code Review)가 보일 것이다.
이 아이콘을 클릭하면 해당 Secene
이 코드 형태로 변경되어 보인다.
Main.storyboard 코드보기
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="k5J-0D-7xo">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19144"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--등록-->
<scene sceneID="6cm-jK-n7h">
<objects>
<viewController id="k5J-0D-7xo" customClass="ViewController" customModule="Experiment_MVC_KVO_NotificationCenter" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="cUl-28-3Xq">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="kyH-h1-fUV">
<rect key="frame" x="114" y="196" width="259" height="34"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="이름" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fll-e2-U2n">
<rect key="frame" x="41" y="203" width="30" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="02K-4W-2ex">
<rect key="frame" x="114" y="253" width="259" height="34"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="전화번호" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="asD-6l-Tb5">
<rect key="frame" x="41" y="260" width="59" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="coda 팬사인회 대기 등록" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1gA-MN-g7z">
<rect key="frame" x="50" y="101" width="364" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="등록 정보" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZtO-Zl-Yfn">
<rect key="frame" x="142" y="549" width="131" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="512-8h-q7c">
<rect key="frame" x="41" y="346" width="332" height="46"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="지금 등록하기"/>
<connections>
<action selector="hitRegisterButton:" destination="k5J-0D-7xo" eventType="touchUpInside" id="RjL-Op-i4O"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="이름" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nK4-hr-JFy">
<rect key="frame" x="50" y="651" width="30" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="전화번호" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Q9-GK-x0p">
<rect key="frame" x="50" y="708" width="59" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="준비중입니다." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IQv-od-aqc">
<rect key="frame" x="142" y="651" width="231" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="준비중입니다." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="695-BK-uot">
<rect key="frame" x="142" y="707" width="231" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mwd-Z5-7B9">
<rect key="frame" x="41" y="416" width="332" height="46"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="tinted" title="등록 확인하기"/>
<connections>
<action selector="hitCheckButton:" destination="k5J-0D-7xo" eventType="touchUpInside" id="MDb-9A-blI"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="6Az-pu-XPI"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<tabBarItem key="tabBarItem" title="등록" id="wvB-cG-y7t"/>
<connections>
<outlet property="nameLabel" destination="IQv-od-aqc" id="oXx-TZ-d2w"/>
<outlet property="nameTextField" destination="kyH-h1-fUV" id="VL2-0D-ZPh"/>
<outlet property="phoneNumberLabel" destination="695-BK-uot" id="lkb-6A-DSJ"/>
<outlet property="phoneNumberTextField" destination="02K-4W-2ex" id="wTy-Lg-QgE"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="1nW-ZH-GWh" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-726.08695652173924" y="-384.375"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
코드는 xml
로 작성되어있고 document
의 타입은 com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB
이다.
대충 훑어 보면 익숙한 단어가 보인다. viewController
, textField
, label
등등.
여기까지 보면 알 수 있다. 우리가 Scene
에서 컴포넌트를 추가하고, 위치를 잡아주고, 크기를 변경하는 작업이 사실은 xib
코드를 작성하고 있었던 것이다.
이제 요소들의 속성을 천천히 살펴보자. 모든 요소는 공통적으로 id
를 가지고 있다.
누가 봐도 직접 입력한 값이 아닌 랜덤으로 부여된 값으로 보인다. 이 중 예제코드 기준으로 textField
의 속성이 id="kyH-h1-fUV"
인 요소를 보자.
이것이 우리가 사용하는 컴포넌트 중 textField
라는 것은 알 수 있으나, ViewController
의 어떤곳과 연결되어 있는지 알 수 없다.
하지만 코드 내부에서 kyH-h1-fUV
를 검색해본다면 다음과 같은 코드를 찾을 수 있다.
<connections>
...
<outlet property="nameTextField" destination="kyH-h1-fUV" id="VL2-0D-ZPh"/>
...
</connections>
힌트를 찾았다. 이것은 아마도 ViewController
에서 @IBOutlet
으로 선언된 nameTextField
프로퍼티에 연결되어 있을것이다.
또한, 코드를 직접 볼 수는 없지만 내부적으로 VL2-0D-ZPh
라는 id
를 통해 연결되어 있을것이라고 유추할 수 있다.
그렇기 때문에 위 예제에서 ViewController
는 View
가 존재하지 않고, 오직 Controller
만 있다고 이야기 할 수 있다.
또한, 코드로 전환되어 있는 모든 Scene
의 요소를 보았기 때문에 StoryBoard
는 MVC 패턴
관점에서 View
라고 볼 수 있다.
하지만 모든 StoryBoard
프로젝트의 ViewContoller
에 View
가 존재하지 않는다는 이야기가 아니다.
해당 예시 코드에만 존재하지 않는다는 것이다.
우리는 여러가지 언어로 개발할 수 있는 사람이지만, 언어마다 문법이 다르다.
누군가는 여러 언어를 번갈아가며 작성해도 문제가 없겠지만, 대부분의 사람들에게는 어렵고 스트레스 받는 일일 것이다.
직설적으로, 이 xib
문서는 사람이 직접 코드로 작성하기에 너무 복잡하다.
그렇기 때문에 사람들은 ViewController
에 코드베이스로 컴포넌트를 생성하고, 위치를 조정하고, 값을 할당하는 작업을 수행한다.
작성된 코드를 해석하는 것은 Cocoa Touch 프레임워크
와 컴파일러가 해야 할 일이다.
이런 관점에서 보면 Cocoa MVC
패턴은 View
와 Controller
를 분리하기 어려운 이유에 대해 명확하게 설명이 가능하다.
실제로는 Controller
와 View
가 명확하게 나뉘어 있지만, 사람들은 빌드가 완료된 코드를 보는 것이 아니기 때문이다.
우리가 코드베이스로 작성한 시점에서 UI를 그리는 코드와 명령을 수행하는 코드가 동시에 ViewController
에 존재하는데 어떻게 이것을 분리하기 쉽겠는가?
실험 2는 여기서 마무리 하겠다.
우리 스티디는 실험 2에 대해 다양한 의견이 있었지만, 해당 내용이 신선하다는 평가가 있어 위키에는 이것만 작성되었다.
위 내용은 의견일 뿐, 해석은 자유다.
Notification
-
NotificationCenter
를 통해 정보를 저장하기 위한 구조체이다.observer
를 등록시켜서 변화를 관찰한다.NotificationCenter
에서 발송된notification
메세지를 전달받아observer
에서 처리한다. - 옵저버 등록 코드
NotificationCenter.default.addObserver(self, selector: #selector(didRecieveTestNotification(_:)), name: NSNotification.Name("TestNotification"), object: nil)
@objc func didRecieveTestNotification(_ notification: Notification) {
print("Test Notification")
}
-
notification
발송 코드
NotificationCenter.default.post(name: NSNotification.Name("TestNotification"), object: nil, userInfo: nil)
- 위와 같이 코드를 짜게될 경우
addObserver
가 버튼이 클릭할 때마다 반복적으로 호출되기 때문에viewDidLoad
에 넣어 한번만 호출될 수 있도록 수정해야 한다
실험 코드 보기
struct UserInfo {
var name: String
var phoneNumber: String
}
class ViewController: UIViewController {
var registrantList: [UserInfo] = []
@IBOutlet weak private var nameTextField: UITextField!
@IBOutlet weak private var phoneNumberTextField: UITextField!
@IBOutlet weak private var nameLabel: UILabel!
@IBOutlet weak private var phoneNumberLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// 노티피케이션 옵저버 등록
NotificationCenter.default.addObserver(self, selector: #selector(update), name: NSNotification.Name.register, object: nil)
}
@IBAction func hitRegisterButton(_ sender: Any) {
let name = nameTextField
.text?
.trimmingCharacters(in: .whitespacesAndNewlines)
let phoneNumber = phoneNumberTextField
.text?
.trimmingCharacters(in: .whitespacesAndNewlines)
let registrant = UserInfo(name: name!, phoneNumber: phoneNumber!)
registrantList.append(registrant)
// 노티피케이션 발송 / 등록된 옵저버와 이름이 동일하기 때문에 selector 실행
NotificationCenter.default.post(name: NSNotification.Name.register, object: nil)
}
@objc func update() {
let lbName = registrantList.last?.name
let lbPhoneNumber = registrantList.last?.phoneNumber
nameLabel.text = lbName
phoneNumberLabel.text = lbPhoneNumber
}
@IBAction func hitCheckButton(_ sender: Any) {
update()
}
}
extension Notification.Name {
static let register = Notification.Name("register")
}
Property Observer-didset, willset
-
Property Observers(프로퍼티 옵저버)를 정의해서 프로퍼티 값의 변화 관찰이 가능하다. 프로퍼티 옵저버는 자신이 정의한 "저장 프로퍼티"에 추가 할 수 있으며, super class(부모클래스)를 상속한 프로퍼티에도 추가 할 수 있다. 프로퍼티 옵저버를 사용하기 위해서는 프로퍼티의 값이 반드시 초기화 되어야 한다.
- willSet : 값이 저장되기 직전에 호출한다. 새로운 프로퍼티의 값에 newValue을 준다.
- didSet : 새로운 값이 저장된 직후에 호출한다. 이전 프로퍼티의 값에 oldValue 을 준다.
-
새 값이 현재 값과 동일한 경우에도 속성에 새 값이 할당될 때 마다 및 옵저버가 호출된다.
Apple 예제 코드
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps