iOS 앱 개발 부트캠프/TIL

TIL 10일차

iosstudyletsgo 2024. 9. 25. 17:55
struct Student{
    let studentName : String
    let studentId : String
}

class StudentManager {
    var studentList : [Student] = []
    var studentDictionary : [String : String] = [:]
    var subjectName : Set<String> = []
    var grade : [String : [Int]] = [:] //과목 이름을 키로 하고, 밸류인 성적을 배열로 저장하는 딕셔너리
    
    func printStudents() {
        print("\n--- 학생 ID와 이름 ---")
        for (id, name) in studentDictionary {
            print("ID: \(id), 이름: \(name)")
        }
    }
    
    func addStudent(name : String, id : String){ // 학생 이름과 아이디 추가
        studentList.append(Student(studentName: name, studentId: id))
        studentDictionary[id] = name
    }
    
    func addSubject(subject : String){  //과목 추가
        subjectName.insert(subject)
    }
    func removeSubject(subject : String){   //과목 삭제 및 해당 과목의 성적 동시 삭제
        subjectName.remove(subject)
        grade.removeValue(forKey: subject)
    }
    
    func addGrade(subject : String, score : Int){
        if grade[subject] == nil {  //딕셔너리에서 키 값(subject)이 nil인지 확인하여 성적이 추가되지 않았는지 체크
            grade[subject] = []     //만약 과목의 성적이 없다면 빈 배열로 초기화하여 성적을 저장할 준비, 딕셔너리에 새로운 과목 추가
        }
        grade[subject]?.append(score)   //grade[subject]가 존재할 때 성적 추가
    }
    func removeGrade(subject : String, score : Int){
        guard let scoreIndex = grade[subject]?.firstIndex(of: score) else { return } //딕셔너리 grade에서 subject라는 키에 대한 배열에서 score의 인덱스 찾기, firstIndex(of:score)->배열에서 첫번째로 일치하는 성적의 인덱스 반환 없으면 nil반환
        grade[subject]?.remove(at: scoreIndex)  //scoreIndex가 유효한 경우 = 해당 성적이 존재하는 경우 해당 인덱스의 성적을 배열에서 제거
    }
    func updateGrade(subject : String, score : Int, newScore : Int){
        guard let scoreIndex = grade[subject]?.firstIndex(of: score) else { return }
        grade[subject]?.remove(at: scoreIndex)  //removeGrade와 같은 방식으로 기존 성적 삭제
        grade[subject]?.append(newScore)        //삭제된 위치에 새로운 성적 추가
    }
}

class ViewController: UIViewController {

    @IBOutlet weak var nameField: UITextField! //이름 입력하는 노란색 textField
    @IBOutlet weak var idField: UITextField!    //id 입력하는 민트색 textField
    @IBOutlet weak var returnButton: UIButton! //입력한 내용 제출하는 버튼
    
    var studentManager = StudentManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        returnButton.layer.cornerRadius = 10
    }
    
    @IBAction func TabButton(_ sender: Any) {
        guard let inputName = nameField.text, !inputName.isEmpty, // 입력된 이름이 비어있지 않을 시 inputName에 내용 저장
              let inputId = idField.text, !inputId.isEmpty        // 입력된 아이디가 비어있지 않을 시 inputId에 내용 저장
        else {
            print("이름과 id를 입력하세요")
            return
        }
        
        studentManager.addStudent(name: inputName, id: inputId)
        studentManager.printStudents()
    }
}

어제 하던 성적관리시스템 코드을 이어서 작성하였다.

어제 하면서 제일 먼저 고치고 싶었던 건 학생별 성적을 관리할 변수의 선언과 학생별 성적 분류 코드였다.

var grade : [String : [Int]] = [:] // 과목명:[점수]

어제 성적을 저장할 변수 grade를 이렇게 선언하였는데, 과목명을 key로, 점수가 저장된 배열을 value로 하는 딕셔너리였다. 코드를 쓸 때는 이정도 하면 성적을 여러개 받아서 저장할 수 있고, 과목별로 여러개의 점수를 관리할 수 있다고 생각했는데 원하는 건 학생별 여러 과목의 점수가 관리되길 원했으니, 이걸론 부족하다.

var grade : [String : [String : [Int]]] = [:] //아이디 : [과목명 : [점수]]

그래서 이렇게 딕셔너리 안에 딕셔너리를 하나 더 늘리는 방식으로 해보려고 한다. 이렇게 하면 특정 학생에 대한 과목별 성적을 불러올 수 있을 것이다.

남은 건 이 변수의 선언이 바뀜으로 인해 바꿔야할 함수들의 코드 변경이다. 이 변수가 사용되는 함수들은 addGrade, removeGrade, updateGrade이다.

    func addGrade(studentId: String, subject : String, score : Int){
        if grade[studentId] == nil {
            grade[studentId] = [:]
        }
        if grade[studentId]![subject] == nil{
            grade[studentId]![subject] = []
        }
        grade[studentId]![subject]?.append(score)
    }

먼저 addGrade인데 매개변수에 studentId: String 부분을 추가했고 if문을 하나 더 추가해 grade[studentId]![subject]가 nil인지 확인하는 부분을 추가했다.

grade[student][subject] 형식의 코드는 중첩 딕셔너리에 접근하는 방식으로, studentId를 키로 하여 과목별 성적 딕셔너리에 접근 한 뒤 다시 subject를 키로 사용해 성적 배열에 접근하는 것이다. 느낌표 !를 붙인 이유는 옵셔널 해제 때문이다. grade[studentId]가 nil이 아니라 이미 과목의 값이 존재한다는 조건에서 사용되기 때문에 강제 언래핑을 한 것이다. 없이 해도 되지만 딕셔너리에 존재하지 않는 키를 조회하면 nil을 반환하기 때문에 따로 옵셔널 처리를 해주어야 한다.

grade[studentId]는 [Stirng : [Int]] 형태인데, 과목별 성적을 담은 딕셔너리를 의미하게 된다. 따라서 특정 과목에 대한 성적을 빈 배열로 초기화하는 grade[subject]=[] 부분을 특정 학생에 대한 과목별 성적을 빈 딕셔너리로 초기화하는 grade[studentId]=[:] 형태로 바꾸었다.

예를 들어 grade에 1번, 2번 학생이 있고 1번 학생의 수학 점수가 100, 90점, 과학 점수가 90, 80점에 2번 학생의 수학 점수가 90, 80점, 과학 점수는 아직 입력되지 않은 상황이라면,

var grade: [String: [String :[Int]]] = [
	"001": [
		"수학": [100, 90],
        	"과학": [90, 80]
        ],
    	"002": [
    		"수학": [90, 80]
	]
]

이런 형태로 값이 있을 것이다. 이 상태에서 grade["001"]["수학"] 형태로 접근하면 1번 학생의 수학 성적 배열인 [100, 90]이 나올 것이다.

정리하면, 첫 if문인 grade[studentId] == nil이 참이라면 특정 학생에 대한 과목별 점수 딕셔너리가 존재한다는 의미이므로 다음 if문을 동작하게 하고, 없다면 빈 딕셔너리를 초기화하는 것이고

grade[studentId]![subject] == nil이 참이라면 특정 학생의 해당 과목의 점수에 대한 배열이 존재하므로 값을 append하는 것이고 거짓이라면 점수 배열이 없으므로 grade[studentId]![subject] = []를 통해 점수를 받기 위한 빈 배열을 초기화 하는 것이다.

이어서 removeGrade와 updateGrade도 같은 방식으로 작성했다.

    func removeGrade(studentId : String, subject : String, score : Int){
        guard let scoreIndex = grade[studentId]?[subject]?.firstIndex(of: score) else { return }
        grade[studentId]?[subject]?.remove(at: scoreIndex)
    }
    
    func updateGrade(studentID: String, subject : String, score : Int, newScore : Int){
        guard let scoreIndex = grade[studentID]?[subject]?.firstIndex(of: score) else { return }
        grade[studentID]?[subject]?.remove(at: scoreIndex)
        grade[studentID]?[subject]?.append(newScore)
    }

매개변수 studentId : String 부분이 추가되고, 어제와 같은 방식이지만 grade[subject]? 였던 부분에 grade[studentId]?[subject]?로 바뀌었다. 

    func averageScore(studentId: String, subject : String) -> Double{
        guard let scoreList = grade[studentId]?[subject] else { return 0 }
        return Double(scoreList.reduce(0, +)) / Double(scoreList.count)
    }

마지막으로 학생별 특정 과목의 평균을 구하는 함수이다. 학생 아이디와 과목명만 입력하면 점수는 불러올 수 있으므로 매개변수는 아이디와 과목명만 지정하였다. 

점수 삭제나 수정과 비슷하게 guard let 구문으로 scoreList에 학생의 특정 과목 점수 배열을 불러와 값이 있을 시 .reduce(0, +)를 통해 초기값을 0으로 하여 배열 내 점수 값을 모두 더하였고, .count로 점수의 개수만큼 나눠 평균을 계산하고 바로 반환하도록 만들었다.

이제 UI를 만들어 코드들과 연결해 완성하려고 했다. 계획은 일단 처음에 만들었던 학생 이름과 아이디를 제출받는 화면은 그대로 두고, 두번째 화면을 만들어 네비게이션 컨트롤러를 통해 연결한 뒤 두번째 화면에서 과목과 성적을 입력 받고 수정, 삭제도 만들고자 했다.

처음엔 두번째 뷰컨트롤러의 클래스도 메인 뷰컨트롤러와 같은 클래스에 연결해 사용해보려고 했는데 UI를 IBOutlet으로 연결하려니 연결이 안되고 무조건 IBAction으로만 연결이 됐다. 혹시나 클래스를 하나로 써서 그런가 싶어서 두번째 뷰컨트롤러는 따로 클래스를 이용해야 하는듯 했다.  그래서 두번째 뷰컨트롤러에서 과목과 성적을 받는 변수를 선언하고, 버튼과 텍스트필드를 연결하고 점수와 과목을 추가하는 함수를 작성하는 정도에서 하루가 끝났다.

원래 두 번째 화면에 여러 UI를 만들어 입력받고, 결과를 띄우려고 했는데 두번째 화면을 잇는 부분이 생각보다 어려워서 그 과정을 찾느라 시간이 너무 오래 걸렸고, 아직 다 이해하지 못한채로 화면 보면서 해보겠다는 생각으로 일단 진행해보는 식이었다. 화면 전환하는 부분에 대해 다시 공부하고 내일 마저 작성해볼 예정이고 이것도 너무 오래걸린다고 하면 일단 놔두고 다음 과제부터 해봐야겠다.

'iOS 앱 개발 부트캠프 > TIL' 카테고리의 다른 글

TIL 12일차  (2) 2024.09.27
TIL 11일차  (1) 2024.09.26
TIL 9일차 - 성적 관리 시스템2  (0) 2024.09.24
TIL 8일차 - 성적 관리 시스템 제작하기  (0) 2024.09.23
TIL 7일차 - 간단한 데이터 타입 연습  (0) 2024.09.20