개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
Stanford Developing iOS11 Apps with Swift를 듣고 정리하였습니다.
MVC 디자인 패러다임
일단 기본적으로 우리 시스템 안의 모든 객체는 세가지로 나뉜다. > Model, View, Controller
1. Model
앱에서 ‘무엇’에 해당하는 UI와 독립적인 객체들이다. 집중력게임에서는 집중력게임을 할 줄 아는 부분에 해당하는 앱이다.
카드가 매치되는지 확인하고, 가져가며 언제 카드를 뒤집어야 하는지 그런 것들을 아는 부분이다. 즉, 집중력 게임에 대한 지식들을 의미한다.
하지만 그게 어떻게 화면에 나오지는 지에 대한 부분은 없다.
2. Controller
‘어떻게’에 해당한다. 어떻게 집중력 게임이 화면에 나오는지에 관심을 갖는다.
Model의 정보를 해석하고 구성해서 View에게 보내주는 역할 혹은 사용자와의 상호작용을 모델에게 해석해주는 역할
3. View
컨트롤러의 하인들로 보통 아주 일반적인 UI 요소들인데(UIButton, UILabel, UIViewController) 컨트롤러가 모델과 통신해 앱에서 어떤 것을 UI에 가져오도록 할 때 필요하다.
이러한 MVC는 세가지 캠프 사이의 커뮤니케이션을 관리하는 데에 의미가 있다. 캠프에 객체를 넣으면 서로 얘기할 때 특정한 규칙을 따라야 한다.
MVC 서로의 관계
Model과 Controller
Controller > Model: Controller가 원하는 대로 Model과 이야기 할 수 있다.
무엇이 어떤건지에 대해 사용자에게 보여줘야 하기 때문에 모델에 대한 접근이 가능해야 한다.
모델의 공개된 모든 기능과는 거의 무제한적인 대화가 가능하다.
Model > Controller: 직접적인 소통은 불가능하다.
Model은 UI와 독립적이고 Controller는 근본적으로 UI에 종속된다.
그러나 소통은 가능하다. Model의 데이터가 변경되었을때 해당 변경사항과 연결된 UI에도 업데이트를 하는 경우!
Notification & KVO(key value observing): Model의 변경사항을 방송하는 것
Model과 View
둘 사이의 소통은 불가능하다.
이유1: 모델은 UI와 독립적이지만, View는 UI에 의존한다.(View는 UI와 관련된 것들만을 포함한다)
이유2: View는 일반 객체일 뿐이다. (버튼 그 자체가 무슨일을 하는지는 알수가 없다.)
View와 Controller
Controller > View: View는 일반적으로 Controller의 하인들이다.
Controller는 outlet을 통해 View 객체들을 모두 관리할 수 있다.
View > Controller: 구조적으로 미리 정해진 방식을 통해 Controller에게 행위에 대한 요청(delegate)과 데이터에 대한 요청(data source)을 할 수 있다.
구조적: 이 통신을 하기로 했을 때 일반적인 객체는 어떻게 컨트롤러와 대화를 할지 조금 먼저 생각해놓고 있어야 한다는 것
더 나아가 targer-action의 구조를 통해 사용자의 행위에 따라 필요한 함수를 호출할 수도 있다.
target: 컨트롤러가 해야하는 건 자신에게 타겟을 만드는 것
action: UIButton이나 다른 것들은 액션을 가지고 버튼을 누를때마다 타겟을 호출
scrollView?
ScrollView가 이미지 같은 걸 스크롤하면 이를 컨트롤러에게 말해줘야 한다.(아래로 더 스크롤할수 있는지, 옆으로 스크롤해도 되는지?와 같은..) 즉, 일을 수행하는 동안 컨트롤러와 대화하길 원한다.
그렇게 하려면 ScrollView가 미리 정의한 메소드를 delegate의 일부로 사용해야 한다.
이 delegate는 ScrollView에 있는 var이고 이 var는 객체를 가지고 있다.
우리가 이 객체에 대해 아는건 이것이 특정 메시지들에 반응을 한다는 것이다.
이는 특정 메시지들에 반응을 한다는 것을 의미한다. 이런 메시지의 대부분에는 will, should, did같은 단어가 들어간다.
view는 일반적(객체)이기 때문에 자기가 화면에 표시하고 있는 데이터를 가질 수는 없다.
즉, 화면에 표시하고 있는 데이터를 인스턴스 변수로 가지고 있지 않다는 뜻이다.
더 나아가 아래와 같은 상황을 가정해보자
View가 아이팟 음악 라이브러리 전체를 보여주고 있다고 생각해보자
노래는 50,000곡이 있다고 가정한 상황에서 ListView(리스트를 만드는 일반적인 View)가 그 50,000개의 곡을 모두 가져오는 것은 불가능할 것이다. 그 대신 프로토콜을 사용해 다른 방식의 특별한 메시지를 주고받는데 (어느곳에 있는 데이터를 줘라, 항목이 몇개 있나 하는 메시지 등) 그러면 Controller는 이를 구현해 Model에게 이야기하여 데이터를 View에 가져다 준다.
즉, TableView는 iOS에 있는 일반적인 뷰 중 하나인데 아이패드에 음악을 스크롤할 때 이게 아이팟 음악 앱이라고 생각을 하고 노래 리스트를 나열해주는 것이 아닌 단지 데이터를 준다고만 인지한 상태에서 화면에 리스트를 띄우게 된다.
이러한 종류의 delegate를 data source라고 한다.
이러한 delegate와 data source는 서로 비슷한데 둘의 차이점은 다른 메소드들을 가지고 있는 것 이다.
이때 메소드들은 UI요소들에 의해 좌우된다.
즉, 미리 정해진 리스트가 아니라 UI요소의 상황에 따라 바뀌는 것을 의미한다.
복잡한 MVC
MVC가 다른 MVC와 소통할 때 다른 MVC를 항상 자신의 뷰로 취급한다.
그래서 세부사항을 모른채로 구조적인 방법으로만 이야기한다. 이는 일반적이고 재사용 가능한 컴포넌트처럼 행동한다.
즉, MVC를 잘 그룹화하는 것이 중요하다!
Model 만들기
Concentration.swift
File > New > File > Swift File > 최상위 폴더아래(ViewController.swift와 동일한 위치)에 생성
Model에 class를 생성할 때에는 공개 API가 무엇인지를 항상 생각해야한다.
API란 Application Programming Interface의 약자로 클래스안의 모든 메소드와 인스턴스 변수의 리스트를 의미한다. 공개 API는 다른 클래스들의 사용을 허락한 메소드와 인스턴스 변수들을 의미한다. 결국 이 클래스를 어떻게 사용하는지를 결정하는 것을 뜻한다.
이때 사용자 입장에서 집중력 게임에서 할 수 있는 일은 카드를 뒤집는 일 밖에 할 수 없다. 다른 카드 매칭 같은 것들은 내부에서 구현해야하는 것이다. 사용자의 관점에서는 오직 카드를 touch하는 행위만 할 수 있다. 그래서 카드를 고르게 해주는 func이 필요하다.
importFoundationclassConcentration{varcards=[Card]()// 빈 배열로 초기화funcchooseCard(atindex:Int){}}
카드를 고를때 인덱스를 이용하게 함으로써 다른 종류의 UI에 대해 좀 더 유연하게 대응해준다.
Card.swift
이제 Card 모델을 만들어보도록 한다. 이 모델또한 UI와는 아무런 관계가 없고 여기서 가장 흥미로운 점은 Card를 구조체로 만드는 것이다.(클래스가 아니다)
구조체와 클래스의 차이
구조체는 상속성이 없다. 이를 통해 구조체를 좀 더 간단하게 만들 수 있다.
구조체는 값 타입이고 클래스는 참조 타입이다.
값 타입은 인자로 보내거나 배열에 넣거나 다른 변수에 할당해도 복사가 가능하다. iOS에서는 배열, 문자열, 정수형, 딕셔너리 모두 구조체이다. 코드안에서 주고받을 때 계속 복사된다. 무언가를 전달할 때 모든 내용을 하나하나 복사하기보다 누군가 내용을 변경했을 때만 실제 복사하도록 하는 전달방식을 취한다.
참조 타입은 힙에 자료형이 담겨있고 그 자료형에 포린터를 쓸 수 있다. 이를 여러군데 사용한다면 실제로 그 자료형을 보내는게 아닌 자료형을 가리키는 포인터를 보내는 것이다. 따라서 코드안에 한 오브젝트를 가리키는 포인터가 잔뜩 있을 수 있다.
다시 Card 모델 생성으로 돌아와서 이 Model안에서는 카드가 무엇을 해야하는지, 어떻게 게임이 진행되어야하는 지에 관한 내용을 작성해준다. Card가 어떻게 보이는지에 대해서는 절대 정의하지 않는다.
importFoundationstructCard{varisFaceUp=false// 뒤집어져 있는 것이 기본varisMatched=false// 서로 일치하지 않는 것이 기본varidentifier:Int// 카드의 식별자}
Controller에서 Model을 써보기
ViewController.swift
Controller에 Concentration 모델클래스를 연결해보자
vargame:Concentration
이제 이 game에겐 어떤 메시지도 전달이 가능해진다. 카드를 가져올수도 있고 카드를 선택할 수도 있다.
그러나 익숙한 에러가 하나 뜰 것이다.
초기화가 되지 않았다는 것으로 아래와 같이 코드를 진행해준다.
vargame:Concentration=Concentration()
신기하게도 해당 방법을 통해 에러를 해결할 수 있게 될 것이다. Concentration는 클래스이기 때문에 모든 변수들이 초기화되면 인수가 없는 init을 자동으로 가지게 된다. Concentration에서는 변수 cards가 딱 하나있고 cards 변수는 이미 초기화가 되어있었다. 따라서 Concentration은 자동으로 init을 가지게 된다.
그리고 iOS는 강한 타입추론이기에 game이 당연히 Concentration 타입이라는 것을 알 수 있기 때문에 아래와 같이 작성이 가능하다.
vargame=Concentration()
자체적인 init을 만들어주기
그러나 우리가 가질 카드가 앞으로 몇개인지는 알수가 없다. 따라서 자체적인 init을 만들어줘야 한다.
Concentration.swift
구조체가 받는 공짜 이니셜라이저는 모든 변수를 초기화한다. 이미 Card에서 isFaceUp, isMatched과 같이 이미 초기화한 것이 있다고 하더라도 이 변수 모두를 초기화해버린다. 따라서 구조체에서 다시 초기화해줌으로써 Card를 초기화한다.
card가 스스로 자기의 식별자를 지정해주게 한다.
카드가 어떤 숫자를 골라도 상관없이 유일하기만 하면 된다. 이것만 충족시키면 된다. 즉 init이 식별자를 받지 않도록!
유일한 식별자를 주기 위해서 Card.swift에서 static 메서드를 사용한다.
importFoundationstructCard{varisFaceUp=falsevarisMatched=falsevaridentifier:IntstaticfuncgetUniqueIdentifier()->Int{// 유일한 Int를 리턴한다return0// 함수가 호출될때마다 유일한 식별자(0)를 리턴할 것이다}init(identifier:Int){self.identifier=0}}
정적함수는 함수이며 Card클래스 안에 있지만 Card에게 보낼 수 없다. Card는 이 메시지를 전혀 이해하지 못하고 메시지를 이해할 수 있는 것은 Card의 타입뿐이다. 이 함수를 전역함수나 유틸리티 함수 도는 그냥 타입에 붙어있는 함수로 생각하면 된다. Card에게 유일한 식별자를 달라고 요청할 수도 없다. Card 타입 자체에게 요청한다.
그래서 함수를 부르고 싶다면 타입에게 부른다. > Card.getUniqueIdentifier()
import Foundation
class Concentration {
var cards = Card
func chooseCard(at index: Int) {
}
init(numberOfPairsCards: Int) {
for identifier in 1...numberOfPairsCards {
let card = Card()
cards += [card, card]
}
} }
스위프트에서의 _ 는 무시하라는 의미이거나 다시 쓰지 않을 거라 어떤 것이어도 상관없다는 의미
카드를 섞기
인스턴스 멤버인 cardButtons를 사용할 수 없다. 이 cardButtons은 속성 이니셜라이저 안에서 사용이 불가능하다. game변수는 속성이고 초기화하고 있으며 속성 이니셜라이저는 self가 존재하기 전에 실행되어야 한다.
스위프트에서는 어떤 거라도 사용하기 전에는 완전히 초기화를 해야한다. 어떤 변수에 접근하거나 어떤 함수를 부르거나 여튼 무엇을 하든 초기화가 되어있어야 한다. 그러나 아직 우리는 game을 초기화하는 중이었기 때문에 완벽한 초기화가 되어있지 않다. 이렇게 지금 상황은 하나가 다른 하나에 의존하고 있는 상황이다.
어떤 변수를 lazy로 만들면 누가 사용하기 전까지는 초기화하지 않는다. 누군가 game을 사용하려 할 때 초기화를 한다. 즉 cardButtons 변수가 초기화될때까지 아무도 game변수를 사용할 수 없다. 이렇게 lazy를 사용하면 초기화가 되었다고 쳐주게 된다. 그러나 이런 lazy를 사용할때 한가지 제약사항이 있다. lazy가 되면 didSet을 가질 수 없다.
카드를 구성하기 위해 Button이라는 객체를 사용하게 된다. 이 Button은 View를 상속받고 있기 때문에 배경화면을 설정할 수 있다.
UI에 있는 항목 조작하기
UI에 있는 항목을 조작하기 위해서는 실행에 대해 코드를 연결해야 한다.
action: 버튼을 눌렀을 때 메서드를 호출하라는 의미
이 메서드는 인수(arguments)를 가질수도 있고 가지지 않을 수도 있다. None 은 인수가 없어도 괜찮은 것이고 이외에는 sender 로 설정. 이 경우에는 UIKit를 보내주는 인수를 의미함으로 인수는 꼭 필요하다.
즉, touchCard가 넘어오면 뒤집을 수 있어야 하는데 이러한 소통을 위해 인수가 존재한다.
Type은 인수의 타입으로 Any가 되어있는데 Button으로 변경해줘야 한다.
버튼이 메소드를 보내주는 것이기 때문으로 만약 UIButton으로 변경해주지 않으면 나머지 코드가 작동하지 않는다.
event는 Touch Up Inside로 경계안에서 터치하고 손을 뗄 때 이 메시지를 보내라는 뜻을 가진다.
IBAction와 _ 의미
13번째 줄이 원으로 바뀌었는데, 원에 마우스를 올려보면 어떤 버튼이 메소드를 부르는지 보여준다.
swift의 모든 인수에는 이름이 있으며 메소드를 부를때 이 이름을 포함해야 한다. 즉, 메소드를 호출하거나 코드를 읽을 때 첫번째 인수가 무엇인지 기억할 필요가 없다.
그리고 이 인수의 이름은 두개로 하나는 호출할 때 사용하는 외부이름 이고 하나는 우리가 코드 구현에 사용할 내부 이름 이다.
이름을 하나만 가지는 것도 가능하고 유효하다. withemoji가 emoji가 된다면 외부, 내부 이름 모두 emoji가 된다.
그리고 만약 인수 앞에 _ 가 있다면 그것은 인수가 없다는 것을 의미한다. (거의 사용하지는 않는다.)
touchCard에는 _ 가 있는데 iOS에서 메시지를 보내는 것으로 이건 Objective-C에서 왔고 여기서는 내부 외부 이름이 없었기 때문에 있다.
@IBActionfunctouchCard(_sender:UIButton)
카드뒤집기
카드를 클릭했을 때 카드가 뒤집히는 효과를 주기 위해 함수를 하나 선언한다. 두개의 인자를 받는 함수이며, 개별 인자에 대해 ‘외부이름’과 ‘내부이름’을 설정한다. 실제 Button을 클릭했을때, flipCard라는 함수가 호출되고 해당 함수에 두가지 인자를 전달하게 된다. 첫번째 인자는 고스트 이모지이며, 두번째 인자는 눌러진 버튼이다. 이렇게 flipCard로 전달된 두개의 인자를 이용해 우리가 원하는 결과(카드 뒤집기)를 만들어낸다.
토글? 하나의 설정값으로부터 다른 값으로 전환하는것으로 오직 두가지 상태밖에 없는 상황에서 스위치를 한번 누르면 한 값이 되고, 다시 한번 누르면 다른 값으로 변하는 것을 의미한다.
몇번 뒤집는지 확인해보기
이와 같은 방식으로 선언을 하게 되면 에러를 만나게 된다.
swift는 모든 인스턴스 변수(속성)은 초기화를 해야한다. 즉, 속성엔 초기값이 항상 있어야 한다.
초기화 방법에는 두가지가 있다.
이니셜라이저를 이용.
이니셜라이저는 단지 특별한 이름을 가진 메소드일 뿐이다. -> init
init은 어떤 인수든 다 가질 수 있고 다른 인수를 가진 여러개의 init을 만드는것도 가능하지만 각 init은 모든 변수를 초기화한다.
값에 0을 쓰는것
varflipCount=0
위와 같이 0을 할당함으로써 간단하게 초기화하도록 한다. swift의 경우 타입에 매우 엄격하다. 사용되는 거의 모든 변수는 타입을 가지며, Objective-C와의 호환을 위해 타입이 없는 경우가 종종있을 뿐이다. 변수의 타입을 명시적으로 적을수도 있지만, 특정 값을 할당하게 되면 해당 변수의 타입을 추론하기도 한다.
다른 카드 만들기 - UILabel 써보기
UILabel: 읽기전용 텍스트 필드
outlet: 인스턴스 변수(속성)를 만든다.
이 속성은 UILabel을 가리키고 횟수가 바뀌면 스스로를 업데이트 하라고 말해준다.
@IBOutletweakvarflipCountLabel:UILabel!
!는 일단 굳이 초기화를 하지 않아도 된다는 특징이 있다. 초기화를 하지않아도 에러가 뜨지 않는다.
모든 속성을 원한다면 속성 다음 didSet이라는 코드를 추가할 수 있다. didSet을 사용하게 되면 매번 flipCountLabel의 텍스트 값을 할당하는 코드를 넣어주지 않고도 원하는 결과값을 얻을 수 있게 된다. 프로퍼티의 값이 바뀐 후에 실행되는 프로퍼티 감시다(didSet)이다.