SSAC_iOS_Day 11 | TIL

린다·2021년 10월 13일
0

SSAC_iOS_Dev

목록 보기
7/33
post-thumbnail

👩‍💻 수업 & 추가 스터디

📂 Custom TableViewCell

  • 먼저 스토리보드에서 TableViewCell 안에 필요한 UI컴포넌트들을 배치 및 오토레이아웃 설정을 해준다.
  • BoxOfficeTableViewCell이라는 UITableViewCell 형식의 Cocoa Touch Class 파일을 생성해주고 UI 컴포넌트들과 IBOutlet 연결을 시켜준다.
class BoxOfficeTableViewCell: UITableViewCell {

    @IBOutlet weak var posterImageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var releaseDateLabel: UILabel!
    @IBOutlet weak var overviewLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}
  • 아웃라인뷰에서 TableViewCell을 잘 선택해준 후 인스펙터 영역에서 BoxOfficeTableViewCell클래스와 연결시켜준다.
  • 각 컴포넌트들에 대한 내용을 cellForRowAt 함수 안에 작성해준다.
class BoxOfficeTableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    // 총 10개의 행 생성
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    	// UITableViewCell 타입의 cell을 BoxOfficeTableViewCell로 as?를 사용하여 타입 캐스팅을 해주었다. 이를 통해 BoxOfficeTableViewCell이 가지고 있는 posterImageView 혹은 titleLabel같은 컴포넌트들에 접근할 수 있게 됐다.
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "BoxOfficeTableViewCell", for: indexPath) as? BoxOfficeTableViewCell else {
            return UITableViewCell()
        }
        
        cell.posterImageView.backgroundColor = .red
        cell.titleLabel.text = "7번방의 선물"
        cell.releaseDateLabel.text = "2021.02.02"
        cell.overviewLabel.text = "영화 줄거리가 보이는 곳 입니다. 영화 줄거리가 보이는 곳 입니다. 영화 줄거리가 보이는 곳 입니다. 영화 줄거리가 보이는 곳 입니다.영화 줄거리가 보이는 곳 입니다. 영화 줄거리가 보이는 곳 입니다."
        cell.overviewLabel.numberOfLines = 0
        
        return cell
    }
    
    // 행의 높이 설정 ➡️ 이때 UIScreen.main.bounds.height를 사용해서 전체 스크린 높이를 활용할 수 있다.
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UIScreen.main.bounds.height / 7
    }
}

cellForRowAt메서드 중

guard let cell = tableView.dequeueReusableCell(withIdentifier: "BoxOfficeTableViewCell", for: indexPath) as? BoxOfficeTableViewCell else { return UITableViewCell() }

이 코드에서 for의 역할이 무엇인지 궁금해서 찾아봤다.

This method uses the index path to perform additional configuration based on the cell’s position in the table view.
이 메서드는 cell의 table view에서의 위치에 기반하여 추가적인 구성을 하기 위해 index path를 사용한다.

📂 TypeCasting

  • 메모리의 인스턴스 타입은 바뀌지 않는 것이 중요함
  • 아래의 코드와 같이 [Any][Int]로 타입캐스팅 했을 때 해당 배열의 값을 꺼내와서 Int로 캐스팅하는거지 기존 배열의 값이 Int로 변하지는 않는다는 뜻
let array: [Any] = [1, 2, 3]
let arrayInt: [Int]? = array as? [Int]
class Mobile {
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

class AppleMobile: Mobile {
    var company = "애플"
}

class GoogleMobile: Mobile {
    
}

let mobile = Mobile(name: "PHONE")
let apple = AppleMobile(name: "iPHONE")
let google = GoogleMobile(name: "Galaxy")

mobile is Mobile // true
mobile is AppleMobile // false
mobile is GoogleMobile // false

apple is Mobile // true
apple is AppleMobile // true
apple is GoogleMobile // false

// Mobile이라고 명확하게 써놓으면 company 프로퍼티에 접근할 수 없음 하지만 접근이 필요할 때 -> 타입 캐스팅이 필요함
let iPhone: Mobile = AppleMobile(name: "iPad")
iPhone as? AppleMobile

if let value = iPhone as? AppleMobile {
    print("성공", value.company)
}

📂 옵셔널 바인딩

// 옵셔널 바인딩: if-let, guard
enum UserMissionStatus: String {
    case missionFailed = "성공"
    case missionSucceed = "실패"
}

func checkNumber(number: Int?) -> (UserMissionStatus, Int?) {
    if number != nil {
        return (.missionSucceed, number!)
    } else {
        return (.missionFailed, nil)
    }
}

func checkNumber2(number: Int?) -> (UserMissionStatus, Int?) {
    if let value = number {
        return (.missionSucceed, value)
    } else {
        return (.missionFailed, nil)
    }
}

func checkNumber3(number: Int?) -> (UserMissionStatus, Int?) {
    guard let value = number else {
        return (.missionFailed, nil)
    }
    
    return (.missionSucceed, value)
}

📂 Struct & Class

  • Struct(구조체)는 Value(값)타입, Class는 Reference(참조)타입이라는 것을 참고해야한다.
// 클래스, 구조체
enum DrinkSize {
    case short, tall, grande, venti
}

struct DrinkStruct {
    let name: String
    var count: Int
    var size: DrinkSize
}

class DrinkClass {
    let name: String
    var count: Int
    var size: DrinkSize
    
    init(name: String, count: Int, size: DrinkSize) {
        self.name = name
        self.count = count
        self.size = size
    }
}
  • Struct의 경우, 값 타입이기 때문에 구조체 자체가 let으로 선언돼있으면 구조체 값 자체에 lock이 걸린다고 생각하면 된다. 따라서 아무리 구조체 안의 프로퍼티들이 변수타입으로 선언돼있어도 구조체 자체가 상수타입이면 프로퍼티가 상수인지 변수인지에 상관없이 수정이 불가능하다.
    ➡️ 구조체 자체를 var로 변경하면 수정 가능하다.
let drinkStruct = DrinkStruct(name: "아메리카노", count: 3, size: .tall)

drinkStruct.count = 2 // error!
drinkStruct.size = .venti// error!
  • Class는 참조 타입이다. 따라서 class를 상수 타입으로 선언해도 전달되는 참조, 즉 메모리의 주소에만 lock이 걸린다. 이는 메모리의 주소가 변경되지 않는 이상 해당 메모리에 할당돼있는 프로퍼티의 타입이 변수라면 수정이 가능하다는 뜻이다.
let drinkClass = DrinkClass(name: "블루베리 스무디", count: 2, size: .venti)

drinkClass.count = 5 // ok
drinkClass.size = .tall // ok

📂 Property
ㄴ 📁 Stored Property(저장 프로퍼티)
ㄴ 📁 Computed Property(연산 프로퍼티)
ㄴ 📁 Type Property(타입 프로퍼티)

📂 Stored Property(저장 프로퍼티)

  • 저장 프로퍼티는 상수(constant)와 변수(variable)값을 인스턴스의 일부로 저장한다.
  • 클래스와 구조체에서만 사용됨

Variable Stored Property(변수)

  • 키워드 var
  • 선언 후에 값을 변경할 수 있다.

Constant Stored Property(상수)

  • 키워드 let
  • 한 번 선언한 후에는 값을 변경할 수 없다.

Lazy Stored Property

  • 값이 사용되기 전까지는 초기화가 되지 않는 프로퍼티

  • 예를 들어 버튼을 누르면 포스터를 보여주는 뷰가 있다. 이때 포스터의 갯수가 1000개인데 사용자가 버튼을 누를지 말지는 이벤트가 발생하기 전까지는 알 수가 없다. 이럴때 포스터 관련된 프로퍼티를 lazy로 선언해주면 사용자가 버튼을 눌러 이벤트를 발생시키기 전까지는 관련 데이터를 불러오지 않아도 되어 효율적으로 메모리를 사용할 수 있다.

  • lazy 프로퍼티를 잘 활용하면 성능도 올라가고 공간 낭비도 줄일 수 있다.

  • 반드시 변수로 선언해야한다.
    let은 한 번 선언이 된 후에는 값을 변경할 수 없다. 따라서 초기화를 함과 동시에 값을 가져야하기 때문에 lazy 프로퍼티로 사용할 수 없다. lazy 프로퍼티는 선언과 초기화가 별도로 이루어지기 때문에.

// 지연 저장 프로퍼티 : 변수 저장 프로퍼티, 선언만 돼있는게 아니라 초기화가 돼있어야함
struct Poster {
    var image: UIImage = UIImage(systemName: "star") ?? UIImage()
    
    init() {
        print("Poster Initialized")
    }
}

struct MediaInformation {
    var mediaTitle: String
    var mediaRuntime: Int
    // 불러주면 그때 초기화
    lazy var mediaPoster: Poster = Poster()
}

var media = MediaInformation(mediaTitle: "오징어게임", mediaRuntime: 333)
print("1")
// 이렇게 접근했을 때 초기화됨
media.mediaPoster
print("2")

📂 Computed Property(연산 프로퍼티)

  • 값을 연산한다.(저장하기 보다는 그때그때 특정 연산을 수행하여 값을 반환한다.)
  • 클래스, 구조체, 열거형에서 사용됨
  • 이때 gettersetter를 사용하여 다른 프로퍼티와 간접적으로 값을 검색하고 세팅한다.
class Point {
    var x: Int {
        get {
            return x
        }
        set(newValue) {
            x = newValue * 2
        }
    }
}
var p: Point = Point()
p.x = 12//error!
  • 위의 코드는 저장소가 없기 때문에 틀린 코드라고 볼 수 있다.
class Point {

    var tempX : Int = 1

    var x: Int {
        get {
            return tempX
        }
        set(newValue) {
            tempX = newValue * 2
        }
    }
}

var p: Point = Point()
p.x = 12
  • getter와 setter를 사용하려면 위의 코드와 같이 연산된 값을 저장할 변수가 반드시 존재해야함
  • 연산 프로퍼티(x)는 반드시 var로 선언돼야함. 값이 고정되어있지 않기 때문에.
  • newValue는 새로운 값을 받아와줄 파라미터 이름. 파라미터를 사용하는 경우, 다른 이름으로 변경해도 무관하다.
class Point {

    var tempX : Int = 1

    var x: Int {
        get {
            return tempX
        }
        set {
            tempX = newValue * 2
        }
    }
}

var p: Point = Point()
p.x = 12
  • 위와 코드와 같이 set을 설정해줄 때 파라미터를 생략할 수도 있다. 하지만 이런 경우에는 반드시 newValue라는 키워드를 사용해야한다.

  • 연산프로퍼티를 불러올 때(변수에 할당할 때?)는 get 메서드가 작동하고 연산 프로퍼티에 새로운 값을 할당할 때는 set 메서드가 작동한다고 봐도 될 것 같다.

  • 연산 프로퍼티에 새로운 값을 할당할 때 할당되는 값의 타입은 연산 프로퍼티 자체의 타입과 동일해야할 것으로 보인다.

  • 연산 프로퍼티를 정의할 때 get만 가지는 것은 가능하나 set만 가지는 것은 불가능하다.

  • get만 있는 연산 프로퍼티를 Read-Only Computed Properties(읽기 전용)이라고 부른다. 그리고 get만 있을 때는 get을 생략해도 된다.

struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double {
        return width * height * depth
    }

📂 Property Observer

  • 프로퍼티 값의 변화를 관찰하고 이에 응답한다.
  • 새로운 값이 프로퍼티의 현재값과 동일하더라도 속성의 값이 설정(set)될 때 마다 호출된다.
  • lazy property를 제외한, 정의된 저장 프로퍼티에 프로퍼티 옵저버를 추가할 수 있다. newValue라는 프로퍼티를 활용할 수 있다.
  • 값이 저장되기 직전에 호출되는 willSet과 값이 저장된 직후에 호출되는 didSet이 있다. oldValue라는 프로퍼티를 활용할 수 있다.
// 연산 프로퍼티 & 프로퍼티 감시자 => Swift 5.1 PropertyWrapper ( @Environment )
class BMI {
    typealias BMIValue = Double
    // 프로퍼티 감시자 추가
    var userName: String {
        willSet {
            print("닉네임 변경 예정: \(userName) -> \(newValue)")
        }
        didSet {
            changeNameCount += 1
            print("닉네임 변경 결과: \(oldValue) -> \(userName)")
        }
    }
    
    var changeNameCount = 0
    
    var userWeight: BMIValue
    var userHeight: BMIValue
    
    var BMIResult: String {
        get {
            let bmiValue = (userWeight * userWeight) / userHeight
            let bmiStatus = bmiValue < 18.5 ? "저체중" : "정상 이상"
            return "\(userName)님의 BMI 지수는 \(bmiValue)으로, \(bmiStatus)입니다."
        }
        //set은 저장 프로퍼티 관련된...
        // :String 생략가능한 이유: 이미 BMIResult가 String이라고 선언돼있어서~ ㄱㅊ
        set(nickname) {
            userName = nickname
        }
        // newValue는 애플에서 지정해준거~
//        set {
//            userName = newValue
//        }
    }
    
    init(userName: String, userWeight: Double, userHeight: Double) {
        self.userName = userName
        self.userHeight = userHeight
        self.userWeight = userWeight
    }
}

let bmi = BMI(userName: "JACK", userWeight: 50, userHeight: 160)
let result = bmi.BMIResult

bmi.BMIResult = "MINSU"
bmi.BMIResult = "JACKIE"
bmi.BMIResult = "HELLO"
print(bmi.changeNameCount)
  • 이를 활용하여 어제 진행했던 프로젝트를 수정해볼 수 있었다.
    list를 table view에 표시하고 add 버튼을 누르면 text view에 있는 내용을 list에 추가, row를 스와이프해서 삭제하면 삭제되는 기능을 구현했었는데 지금은 단순하지만 추후에 기능이 늘어나기 시작하면 tableView.reloadData()를 추가해줘야하는 시점을 정확하게 설정하는 것이 어려워질 수 있다.
  • 이럴때를 대비하여 기능기능마다 reload하는 메서드를 추가해주는 것이 아니라 데이터 list 자체에 Property Observer(didSet)을 추가하여 list가 변화가 일어날 때마다 tableView를 reload를 시켜줄 수 있다.
    var list: [String] = ["장 보기", "메모메모", "영화 보러 가기", "WWDC 시청하기"] {
	//list에 변화가 일어날 때 실행되는 메서드
        didSet {
            tableView.reloadData()
        }
    }

📂 Type Property(타입 프로퍼티)

  • 일반적으로 저장 프로퍼티와 연산 프로퍼티는 특정 타입의 인스턴스와 연결됨
  • 하지만 프로퍼티를 타입 자체와 연결하는 것도 가능하다.

👩‍💻 Mission

📂 Property Wrapper

0개의 댓글