개발에 있어서 디자인 패턴 = 설계도를 어떻게 그릴지에 대한 가이드 이다.
: 객체지향적인 디자인 패턴
iOS 개발 시 항상 염두에 두어야 하는 디자인패턴.
시스템 안의 모든 객체를 모델/뷰/컨트롤러라는 세 가지 캠프로 나눠 볼 수 있다.
모델, 뷰, 컨트롤러라는 세 캠프 사이의 커뮤니케이션을 관리하는 데에 의미가 있다.
: 거의 한 번에 모두와 이야기할 수 있다.
뷰 -> 컨트롤러 : 블라인드 커뮤니케이션. 대화는 가능하나, 구조적이고 블라인드 상태인 대화여야 한다. (Blind-Communication)
예를 들어 버튼이 클릭된 경우에 해당
블라인드 상태 : 뷰 입장에서는 컨트롤러에 대한 정보가 전혀 없어야 한다.
구조적 structured : 뷰의 일반적인 객체가 컨트롤러가 대화하는 방법이 미리 정해져 있다(pre-defined).
타겟 액션 target-action 처럼
- 타겟 : 컨트롤러는 자신에게 타겟(메서드 등)을 만든다
- 액션 : 뷰(버튼 등)는 액션을 가지고, 버튼이 눌릴 때마다 타겟을 호출한다.
delegate 델리게이트 : 행위에 대한 요청
data source : 데이터에 대한 요청
정리하자면..
하나의 MVC모델은 아이폰/아이패드의 스크린 "하나"를 제어한다.
MVC모델은 항상 UI 그룹과 함께 다닌다.
여러 개의 스크린을 가지는 앱은 당연히 여러 개의 MVC모델을 가져야 한다.
MVC가 다른 MVC와 소통할 떄는, 항상 다른 MVC를 "뷰"의 일부로 취급한다.
❗️각 MVC의 컨트롤러가 뷰 외에 아무 곳이나 소통하면 절대 안 된다!
스위프트 파일을 선택해 생성.
스위프트 파일 이름을 지을 때는, 언제나 그 파일 내에서 가장 중요한 클래스 이름을 따 온다.
새로운 클래스를 만들고 나면 항상 그 클래스의 공개 API (public API)가 무엇인지 생각해야 한다.
스위프트에서 구조체와 클래스는 거의 똑같다.
✅ 중요한 차이점 1 : 구조체는 상속성이 없다
✅ 중요한 차이점 2 : 구조체는 값 타입이고, 클래스는 참조 타입이다.
(추가) 클래스는 모든 프로퍼티가 초기화 되면 인수가 없는 init을 자동으로 가지게 된다. 구조체는 모든 프로퍼티를 초기화 할 수 있는 init을 자동으로 가지게 됩니다.
값 타입은 "복사"된다.
값 타입을 전달할 때는 그 값이 복사되어 전달된다.
정확하게는 스위프트에서 값 타입 복사는 "쓰기 시 복사" 체계로 구현되어 있다.
쓰기 시 복사 Copy on Write : 변경사항이 있을 때만 실제로 복사함
참조 타입은 포인터를 사용한다.
✅ API : Application Programming Interface, 클래스 안의 모든 메소드와 인스턴스 변수의 리스트.
public API : 다른 클래스들의 사용을 허락한 메소드와 인스턴스 변수들.
이 클래스를 어떻게 사용하는지 정하는 것.
클래스의 모든 프로퍼티가 초기화되어 있다면 기본 이니셜라이저가 자동 생성된다.
구조체의 기본 이니셜라이저의 경우, 모든 프로퍼티가 기본값이 있어도 모든 변수를 초기화한다.
init은 내부 이름과 외부 이름이 종종 같아지는 유일한 메서드이다.
스위프트 반복문 : for ... in 시퀀스.
여기서 시퀀스란 어딘가에서 시작해 그 다음, 그 다음 .. 으로 넘어갈 수 있는 것은 모두 시퀀스이다.
배열, 문자열,
: ..<, ... 과 같은 범위 표현
정적 메서드
Card.getUniqueIdentifier()
라고 하는 것이다. Card 타입 자체에 요청하기 때문정적 변수
_
: 무시하라는 의미 or 다시 쓰지 않을 값이기 때문에 어떤 것이어도 상관 없다는 의미를 가짐.
: 해당 변수를 누군가 사용하기 전까지는 초기화하지 않겠다는 의미.
lazy var game = Concentration(numberOfPairsOfCards: cardButtons.count / 2)
@IBOutlet var cardButtons: [UIButton]!
cardButtons 가 초기화되기 전까지는 game도 초기화되지 않지만, lazy 키워드 덕분에 초기화되지 않았어도 초기화되었다고 쳐 준다.
※ lazy가 되면 프로퍼티 감시자를 가질 수 없다.
뷰에서 touchCard()를 선택했을 때, game.chooseCard() 를 부르면 모델의 데이터가 변경된다.
❗️뷰는 모델로부터 변경된 데이터를 가져와야 한다 (뷰와 모델을 동기화해야 한다)
그 역할을 위해 updateViewFromModel() 메서드를 만든다.
: 배열의 메소드. 모든 인덱스의 계수 가능 범위를 배열로 리턴해 줌
배열의 모든 인덱스를 돌아야 할 때 활용하기 좋다.
: 키와 값 쌍으로 나열된 자료구조.
리턴타입은 옵셔널이며, 찾는 값이 없을 때는 설정되지 않은 옵셔널(nil)을 반환한다.
??
: 옵셔널을 다루는 또 다른 방법 if emoji[card.identifier] != nil {
return emoji[card.identifier]!
} else {
return "?"
}
위 코드와 아래 코드는 완전히 동일하다.
return emoji[card.identifier] ?? "?"
??
는 앞의 대상이 nil이 아니라면 그 대상을, nil이라면 뒤의 대상을 반환하는 연산자이다.
arc4random_uniform(__upper_bound:)
유사 임의 번호 생성기. 0부터 __upper_bound 사이의 숫자를 임의로 생성함. upper bound 숫자는 포함하지 않음. UInt에서만 잘 작동함.
스위프트에서 중첩된 if문은 그냥 if 뒤에 조건을 나열하면 된다.
(= 유일하게 앞면인 단 하나인 카드의 인덱스를 나타내는 변수)
만약 앞면인 카드가 아무것도 없다면, 또는 앞면인 카드가 여러 개라면 해당 값은 없게 된다(nil).
이럴 때, 이 값은 설정되지 않는 게 좋다. 따라서 이럴 때 옵셔널을 사용한다.
뒤집혀진 카드의 숫자를 Tracking 하는 변수를 하나 설정하겠습니다. 어떤 카드도 뒤집혀 있지 않은 상태(== nil)가 있을 수 있기 때문에 옵셔널로 타입을 정합니다.
var indexOfOneAndOnlyFaceUpCard: Int?
첫번째 분기에서, matchIndex에 뒤집혀진 카드의 index 값을 넣고, 뒤집힌 카드가 동일한 카드임을 방지하기 위해 matchIndex != index를 선언합니다. 만약 두개의 조건이 사실이라면,
실제 두개의 카드가 일치하는지 확인하게 됩니다.
일치하기 때문에 각각의 카드가 가지고 있는 isMatched에 대한 Bool 값을 true로 바꿔줍니다.
일치하지 않은 경우에 대해서 사용자가 선택한 카드에 대한 isFaceUp은 true 값을 가지게 되고
두장의 카드가 뒤집혀 있는 상태이니 indexOfOneAndOnlyFaceUpCard는 nil값을 가져야 합니다.
일치하지 않는 경우 모든 카드를 다시 뒤집어야 하고 사용자가 선택한 값은 true로 만들고 indexOfOneAndOnlyFaceUpCard에 사용자가 선택한 index라는 매개변수로 indentifier 값을 할당해 줍니다.
ViewController.swift
//
// ViewController.swift
// Concentration
//
// Created by Bibi on 2021/09/15.
//
import UIKit
class ViewController: UIViewController {
lazy var game = Concentration(numberOfPairsOfCards: cardButtons.count / 2) // 컨트롤러가 모델에게 이야기하는 창구.
var flipCount = 0 {
didSet {
flipCountLabel.text = "Flips: \(flipCount)"
}
}
@IBOutlet weak var flipCountLabel: UILabel!
@IBOutlet var cardButtons: [UIButton]!
@IBAction func touchCard(_ sender: UIButton){
flipCount += 1
if let cardNumber = cardButtons.firstIndex(of: sender) {
game.chooseCard(at: cardNumber)
updateViewFromModel()
} else {
print("chosen card was not in cardButtons.")
}
}
func updateViewFromModel() {
for index in cardButtons.indices {
let button = cardButtons[index]
let card = game.cards[index]
if card.isFaceUp { // 앞면이면 카드 내용 보이기
button.setTitle(emoji(for: card), for: UIControl.State.normal)
button.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
} else {
button.setTitle("", for: UIControl.State.normal)
button.backgroundColor = card.isMatched ? #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0) : #colorLiteral(red: 0.9411764741, green: 0.4980392158, blue: 0.3529411852, alpha: 1)
}
}
}
var emojiChoices = ["🦇", "😱", "🙀", "😈", "🎃", "👻", "🍭", "🍬", "🍎"]
var emoji = [Int:String]()
func emoji(for card: Card) -> String {
// emoji 딕셔너리를 요청할 때만 이모지를 랜덤으로 넣어준다.
if emoji[card.identifier] == nil, emojiChoices.count > 0 {
let randomIndex = Int(arc4random_uniform(UInt32(emojiChoices.count)))
emoji[card.identifier] = emojiChoices.remove(at: randomIndex) // emojiChoices에서 사용한 이모지는 제거함
}
return emoji[card.identifier] ?? "?"
}
}
Card.swift
//
// Card.swift
// Concentration
//
// Created by Bibi on 2021/09/22.
//
import Foundation
struct Card {
var isFaceUp = false
var isMatched = false
var identifier: Int
static var identifierFactory = 0
static func getUniqueIdentifier() -> Int {
// 정적 메서드 안에서는 Card.없이 정적 변수에 접근 가능
identifierFactory += 1
return identifierFactory
}
init() {
self.identifier = Card.getUniqueIdentifier()
}
}
Concentration.swift
//
// Concentration.swift
// Concentration
//
// Created by Bibi on 2021/09/22.
//
import Foundation
class Concentration {
var cards = [Card]() // 빈 배열
var indexOfOneAndOnlyFaceUpCard: Int? // 한 카드만 앞면인지 추적하기 위한 변수 - 옵셔널
func chooseCard(at index: Int) {
// 가장 먼저 - 이미 매칭되지 않은 카드일 때만 실행
if !cards[index].isMatched {
if let matchIndex = indexOfOneAndOnlyFaceUpCard
, matchIndex != index {
// 카드가 매칭되었는지 확인
if cards[matchIndex].identifier == cards[index].identifier {
cards[matchIndex].isMatched = true
cards[index].isMatched = true
}
// 매칭되지 않았지만 두 번째 카드를 선택했을 때 - 카드의 앞면을 보여주어야 함
cards[index].isFaceUp = true
indexOfOneAndOnlyFaceUpCard = nil // 이 시점에는 두 장의 카드가 앞면이기 때문에 유일하게 앞면인 카드가 없게 된다
} else {
// 앞면인 카드가 없거나, 두 카드가 앞면인 경우 (매칭 불가능)
// 카드를 모두 다시 뒤집어 사용자가 선택한 카드만 앞면일 수 있게 한다. 그 카드의 인덱스는 indexOfOneAndOnlyFaceUpCard 가 된다.
for flipDownIndex in cards.indices {
cards[flipDownIndex].isFaceUp = false
}
// 사용자가 선택한 그 카드는 앞면을 보여줌 - 유일하게 앞면인 카드
cards[index].isFaceUp = true
indexOfOneAndOnlyFaceUpCard = index
}
}
}
init(numberOfPairsOfCards: Int) {
// 사람들이 집중력 게임을 만들 때 필요한 것 : 카드 쌍의 갯수
for _ in 1...numberOfPairsOfCards {
let card = Card()
cards += [card, card]
}
// TODO: Shuffle the Card - 카드를 섞지 않으면 항상 같은 카드가 나올 것이므로.
}
}