[새싹 iOS] 3주차

임승섭·2023년 8월 4일
0

새싹 iOS

목록 보기
7/45

화면 전환

스토리보드 내에서 인터페이스 빌더 활용

2주차_#다음 화면 전환

  • 화면 전환에 대한 방향 파악과 구현이 쉽다
  • 세부적인 컨트롤(사용자의 상태에 따라 다른 화면, ...) 불가능
    • 지난주에 그래서 못한 부분이 있었다. (SignUp Screen)

코드 구현

  • 전환 방식
    • 아 -> 위 (Modal) : present - dismiss

      	
      // 이전 화면. nextButtonTapped()
      present(vc, animated: true, completion: nil) 
      
      // 다음 화면. closeButtonTapped()
      dismiss(animated: true, completion: nil)
    • 오 -> 외 (Show) : push - pop

      • 스토리보드 상에서 Navigation Controller가 임베드 되어있어야 한다
      
      // 이전 화면. nextButtonTapped()
      navigationController?.pushViewController(vc, animated: true)
      
      // 다음 화면. closeButtonTapped()
      navigationController?.popViewController(animated: true)
      • 디폴트로 back button이 구현되어 있지만,
        원하면 버튼을 추가로 구현할 수 있다

데이터 전송 (변경..?)

A -> B 데이터 전송

  1. (B) 데이터 받을 변수(프로퍼티) 선언
    var contents: String?
  2. (A) 해당 vc의 프로퍼티 값 변경
    vc.contents = list[indexPath.row]
  3. (B) 전달받은 값 뷰에 표현
    mainLabel.text = contents
  • 다음 화면으로 전환될 때 데이터를 넘겨주는 느낌일 줄 알았는데
    그냥 해당 화면의 프로퍼티를 변경하는 느낌이 강하다
  • (주의)
    데이터 전송 시, 1번 생략하고 그냥 아웃렛에 접근해서 값을 전달하는 건 불가능하다.
  • 일단 지금은 초기화되는 시점이 다르다 정도로 알고 있자
    (A) ** mainLabel.text = list[indexPath.row]

유동적인 셀 높이 (Dynamic Height)

  • 셀 안의 text에 따라 높이가 유동적으로 변하게 하는 방법
  1. AutoLayout 시 높이 지정x
    • 당연히 높이 지정해버리면 안된다
  2. label의 numberOfLines
    • 당연히 여러 줄 보여야 하니까 0으로 설정한다
      cell.contentLabel.numberOfLines = 0
  3. automatic dimension
    • 처음 본다
    • 높이를 자동으로 지정(?)해준다
      tableView.rowHeight = UITableView.automaticDimension
      • viewDidLoad() 안에 써주자

셀 스와이프

  • 특정 셀을 스와이프했을 때 기능이 구현되도록 한다

시스템

  1. 편집 상태로 바꿔준다

    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
            return true
    }
  2. 편집한다

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
            list.remove(at: indexPath.row)
            tableView.reloadData()
    }

커스텀

  • leading과 trailing을 이용해서 다른 기능을 구현할 수 있다
  • 연습해보쟈

XIB Cell

  • 셀 파일을 XIB파일과 함께 만들면 셀만 따로 작업할 수 있는 인터페이스 빌더를 만들 수 있다
  1. cell 파일 따로 생성 (XIB 체크)
    • class는 자동으로 연결
    • identifier는 직접 연결해야 함
  2. controller 파일로 돌아와서 nib 등록
    let nib = UINib(nibName: "TestCollectionViewCell", bundle: nil)
    mainTableView.register(nib, forCellWithReuseIdentifier: "TestCollectionViewCell")

Delegate, DataSource

  • UITableViewController는 UITableView에 필요한 프로토콜이 기본적으로 내장되어 있기 때문에, 따로 프로토콜을 채택할 필요가 없었다
  • 하지만 UIViewController 위에 UITableView를 올려서 사용하려면
    프로토콜을 따로 채택해주어야 메서드를 호출할 수 있다
  1. 프로토콜 채택
  2. 뷰와 연결
  3. 뷰 아웃렛
// 1
class MainViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { // 1

	// 3
	@IBOutlet var mainTableView: UITableVIew!
    
    override func viewDidLoad() {
    	super.viewDidLoad()
        
        // 2
        mainTableView.delegate = self
        mainTableView.dataSource = self
    }
}

Delegate

  • 어떤 행동에 대한 동작 제시
  • 동작/행동을 공급하는 역할 (supplies the behavior)
  • MVC의 C
  • 필수 (TableView)
    • cellForRowAt
    • numberOfRowsInSection

DataSource

  • 데이터를 받고 보여주는 것 담당
  • 데이터를 공급하는 역할 (supplies the data)
  • MVC의 M

enum + 연산 프로퍼티

  • enum + CaseIterable, allCases 굿
  • 타입 프로퍼티 사용 주의
    • 인스턴스 프로퍼티 사용 -> 타입 이름().프로퍼티
    • 타입 프로퍼티 사용 -> 타입 이름.프로퍼티
  • enum + 연산 프로퍼티 조합이 아주 깡패다

    • 코드 완전 간결하게 사용 가능
    enum SettingOptions: Int, CaseIterable {
        case total, personal, others	// 그냥 케이스
    
        // enum에서 타입 프로퍼티는 사용 가능!!
        // rawValue는 Int이지만, 타입 프로퍼티로 String을 받아올 수 있다!!
        var mainOptions: String {   // (섹션 이름)
            // get만 쓸 때는 생략 가능
            //get {
                switch self {
                case .total : return "전체"
                case .personal : return "개인"
                case .others : return "기타"
                }
            //}
        }
    
        var subOptions: [String] { // (셀 이름)
            switch self {
            case .total : return ["공지사항", "실험실", "버전 정보"]
            case .personal : return ["노트북", "핸드폰", "충전기", "케이블"]
            case .others : return ["애플", "삼성"]
            }
        }
    }
  • 실제 코드에 적용

// 1. 섹션 개수
return SettingOptions.allCases.count

// 2. 섹션 텍스트
return SettingOptions.allCases[section].mainOptions

// 3. 셀 개수
return SettingOptions.allCases[section].subOptions.count

// 4. 셀 텍스트
cell.textLabel?.text = SettingOptions.allCases[indexPath.section].subOptions[indexPath.row]

awakeFromNib

  • 객체의 초기화(인스턴스화) 후 호출
  • 재사용 메커니즘에 의해 셀이 계속 재활용되는데,
    모든 셀의 공통적인 속성(텍스트 크기, 폰트, ...)를 매번 새로 업데이트해줄 필요가 없다.
  • 데이터만 업데이트하면 되자너
  • 이 경우 awakeFromNib() 내에 정적인 데이터를 선언한다
override func awakeFromNib() {
	super.awakeFromNib()
    
    mainTitleLabel.font = .boldSystemFond(ofSize: 17)
    mainTitleLabel.textColor = .brown
}

TextView placeholder

  • 스토리보드 상에서 textview를 코드로 땡겨오면
    outlet만 연결이 가능하고, action 연결은 불가능한 걸 확인할 수 있다
  • label과 button은 기능이라고 해봤자 하나밖에 없지만,
    textview나 searchbar 등은 다양한 액션이 존재한다
  • 이런 많은 액션을 감당하기 위해 프로토콜을 이용한다
  • TextViewDelegate를 채택한다
  • textView에는 placeholder를 지정하는 별도의 메서드가 따로 없다
  • 그래서 커서의 움직임을 감지하는 타이밍으로 placeholder를 구현한다
// 프로토콜 채택
class DetailViewController: UIViewController, UITextViewDelegate {
	
  // placeholder로 사용할 텍스트
  var placeholderText = "내용을 입력해주세요"
  
  // outlet 연결
  @IBOutlet var contentTextView: UITextView!

  // 초기 설정
  override func viewDidLoad() {
  	  contentTextView.delegate = self
      
      if contentTextView.text.isEmpty {
              contentTextView.text = placeholderText
              contentTextView.textColor = .lightgray
      }
  }

  // 커서가 깜빡깜빡
  func textViewDidBeginEditing(_ textView: UITextView) {
      if textView.text == placeholderText {
          textView.text = ""
          textView.textColor = .blace
      }
  }

  // 커서 없어짐
  func textViewDidEndEditing(_ textView: UITextView) {
      if textView.text.isEmpty {
          textView.text = placeholderText
          textView.textColor = .red
  }
}

PickerView 등장

  • 스토리보드 상에 pickerView를 그냥 띄워줄 수는 있따
  • 하지만, 일반적으로 어플을 사용할 때 화면에 띵 하고 pickerView가 있는 경우는 잘 없다
  • 특정 버튼을 클릭했을 때, 아래에서 pickerView가 올라오는 화면이 일반적이다
  • 이걸 어떻게 구현하냐
  • textfield를 이용한다
  • textfield를 터치하면 키보드가 올라오는게 정상인데,
    키보드 대신 pickerView가 올라오게 할거다
  • textfield는 버튼인 척 해야 하기 때문에 커서 깜빡이는 것도 없애준다
// 프로토콜 채택
class LottoViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

  // 버튼인 척 할 textfield 아웃렛
  @IBOutlet var numberTextField: UITextField!

  // pickerView는 스토리보드에 얹는 게 아니고,
  // 빈 상태로 코드에서 만들어준다
  let pickerView = UIPickerView()

  // pickerView에 뜨게 할 요소들
  var list: [Int] = Array(1...1000).reversed()



  override func viewDidLoad() {
          super.viewDidLoad()

          // 키보드가 떠야 하는 inputView를 바꿔버려
          numberTextField.inputView = pickerView  

          // 커서도 안보이게 clear 색깔로 해버리자
          numberTextField.tintColor = .clear 

          // 프로토콜 연결
          pickerView.delegate = self
          pickerView.dataSource = self
  }
  

  // 함수 4개 선언
  func numberOfComponents(in pickerView: UIPickerView) -> Int {
          return 1
  }

  func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
          return list.count
  }

  func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
          numberTextField.text = "\(list[row])"
  }

  func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
          return "\(list[row])"
  }
}

사용자의 상태에 따른 화면 교체

  • 각 버튼을 누르면 다음 화면으로 넘어간다
  • 마지막 로그아웃 버튼 누르면 뷰가 다 초기화되고 온보딩 화면으로 돌아간다
  • 로그인 버튼을 누르면 로그인o 상태
  • 로그아웃 버튼 누르면 로그인x 상태
  • 로그인o 상태에서 앱을 실행시키면 온보딩 화면이 첫 화면
  • 로그인x 상태에서 앱을 실행시키면 메인화면이 첫 화면

코드

  • Onboarding Screen

    // 할 일 : 1. login 상태 2. 화면 전환
    @IBAction func loginButtonTapped(_ sender: UIButton) {
    
            // login되었다고 상태 저장
            UserDefaults.standard.set(true, forKey: "isLogin")
            print("UserDefault 변경. true")
    
            // 다음 화면 넘어감
            // 1 + 2. 같은 스토리보드
            let vc = storyboard?.instantiateViewController(withIdentifier: "MainViewController") as! MainViewController
    
            // 3. 화면 전환 방식
            vc.modalPresentationStyle = .fullScreen
    
            // 4. 화면 띄우기
            present(vc, animated: true)
    }
  • Main Screen

    // 할 일 : 1. 화면 전환
    @IBAction func settingButtonTapped(_ sender: UIButton) {
            // 화면 이동
            // 1 + 2. 같은 스토리보드
            let vc = storyboard?.instantiateViewController(withIdentifier: "SettingViewController") as! SettingViewController
    
            // 3. 화면 전환 방식
            vc.modalPresentationStyle = .fullScreen
    
            // 4. 화면 띄우기
            present(vc, animated: true)
    }
  • Setting Screen

    // 할 일 : 1. logout 상태 2. rootview 수정
    @IBAction func logoutButtonTapped(_ sender: UIButton) {
            // 1. userdefault 수정
            UserDefaults.standard.set(false, forKey: "isLogin")
            print("UserDefault 변경. false")
    
            // 2. 루트 뷰까지 초기화
            let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
            let sceneDelegate = windowScene?.delegate as? SceneDelegate
    
            let vc = storyboard?.instantiateViewController(identifier: "OnboardingViewController") as! OnboardingViewController
            sceneDelegate?.window?.rootViewController = vc
            sceneDelegate?.window?.makeKeyAndVisible()
    }

뷰의 생명주기

  • 마지막 로그아웃 버튼을 눌렀을 때, 기존에 쌓여있던 화면들이 어떻게 없어지는지 확인하고 싶었따
  • 생명주기 함수들에 print(#function)을 넣어서 로그가 찍히게 했다
  • 로그인o

    // 온보딩 화면
    UserDefaults : false
    onboarding viewDidLoad()
    onboarding viewWillAppear(_:)
    onboarding viewDidAppear(_:)
    
    // 로그인 버튼 클릭 -> 메인화면
    UserDefault 변경. true
    main viewDidLoad()
    onboarding viewWillDisappear(_:)
    main viewWillAppear(_:)
    main viewDidAppear(_:)
    onboarding viewDidDisappear(_:)
    
    // 설정 버튼 클릭 -> 설정화면
    setting viewDidLoad()
    main viewWillDisappear(_:)
    setting viewWillAppear(_:)
    setting viewDidAppear(_:)
    main viewDidDisappear(_:)
    
    /* 여기까지는 일반적인 화면 전환 시 생명주기 함수의 실행과 동일*/
    
    // 로그아웃 버튼 클릭 -> 온보딩 화면
    UserDefault 변경. false
    setting viewWillDisappear(_:)
    main viewWillAppear(_:)
    main viewDidAppear(_:)
    setting viewDidDisappear(_:)
    
    main viewWillDisappear(_:)
    onboarding viewWillAppear(_:)
    onboarding viewDidAppear(_:)
    main viewDidDisappear(_:)
    
    onboarding viewWillDisappear(_:)
    onboarding viewDidLoad()
    onboarding viewWillAppear(_:)
    onboarding viewDidDisappear(_:)
    onboarding viewDidAppear(_:)
    
    /* 초기화면도 onboarding이었기 때문에 두 onboarding이 겹친다 */
  • 로그인x

    // 메인 화면
    UserDefaults : true
    main viewDidLoad()
    main viewWillAppear(_:)
    main viewDidAppear(_:)
    
    // 설정 버튼 클릭 -> 설정 화면
    setting viewDidLoad()
    main viewWillDisappear(_:)
    setting viewWillAppear(_:)
    setting viewDidAppear(_:)
    main viewDidDisappear(_:)
    
    /* 여기까지는 일반적인 화면 전환 시 생명주기 함수의 실행과 동일*/
    
    // 로그아웃 버튼 클릭 -> 온보딩 화면
    UserDefault 변경. false
    setting viewWillDisappear(_:)
    main viewWillAppear(_:)
    main viewDidAppear(_:)
    setting viewDidDisappear(_:)
    
    main viewWillDisappear(_:)
    onboarding viewDidLoad()
    onboarding viewWillAppear(_:)
    main viewDidDisappear(_:)
    onboarding viewDidAppear(_:)
  • 로그아웃 버튼을 누르면, 이제껏 쌓여있던 화면들이 역순으로 순식간에 전환이 일어나는 걸 확인할 수 있다.

  • 특이한 점
    • 일반적인 화면 전환 시에는 다음 화면의 viewDidAppear() 후 이전 화면의 viewDidDisappear()가 실행되었는데
    • 루트 뷰 초기화 과정에서는 순서가 바뀌어있음을 확인할 수 있다.

1개의 댓글

comment-user-thumbnail
2023년 8월 4일

감사합니다. 이런 정보를 나눠주셔서 좋아요.

답글 달기