본문 바로가기

iOS

[Memo] 메모 앱을 만들어보자. (전개)

728x90

사실 전개 + 위기 + 절정이 한번에 있음 ..

 

그럼에도 전개를 써보자.

인생은 단계별로 가야하는 것 ..

 

초기세팅

제일 설레면서 제일 귀찮은 작업이랄까.

여기서 꼬이면 정말 하기 싫어짐. (이런 내가 개발자를 해도 될까?)

 

초기세팅은 사실 별거 없고 .. 크게 두가지로 나뉘어지는데 .. (나같은 경우엔)

Github

레포지토리 만들고 .. (과제 기간 중이라서 Private인데 .. 북흐러우면 계속 Private으로 할 것임. 그렇다고 지금 Public인 프로젝트가 .. 자랑스럽다 .. ? 는 아니고 ㅋㅋ .. 제 master piece는 WAL 그리고 Dear today뿐.)

 

그리고 라벨을 만듭니다. 사실 안만들어도 됨. 솝트 습관임.

 

프로젝트도 만들어요. 이것도 역시 솝트 습관임.

 

*이번에 프로젝트가 업데이트 된 것 같은데 ..

이렇게 볼 수 있고, 날짜 이런거 컬럼 추가해서 공수산정 할 때 사용해도 좋을 것 같다.

여기서 추가하면 이슈로 만들어져서 편리 .. (하다고 한다. thx to takki)

왜 아직 정리안하심요? 빨리 하삼.

그렇다고 합니다.

 

Project 

 

그리고 리모트 레포지토리랑 로컬 프로젝트 연결해주고 ..

.gitignore 파일 만들고 프로젝트 추가하면 .. 이제야 발단 정도 .. ? ㅋ ㅋ ..

 

폴더링 해주시고 ~

필요한 SPM 파일을 넣어주시면 ~

 

이제 슬 작업을 해보자 ~ (아직까지 발단인걸까?)

 

작업 Start

가보자고 ..

 

일단 .. 초기에 필요한 파일들 예를 들면 Util 관련 파일 .. Realm 관련 파일 .. Network를 한다면(= 서버가 있다면) 상수 코드 등등의 파일을 추가한다.

 

나는 이런 파일들을 Global 또는 Network로 폴더링을 해서 관리를 하는데,

이렇게 나눠준다.

각 폴더를 살펴보면 Global 폴더는 프로젝트 전반적으로 사용할 코드들을 모아놓은 곳이다.

  • Constant 
    UserDefaults에서 사용하는 Key값 String과 같은 상수들 
    프로젝트 전반에서 변하지 않는 값들 ..
  • Realm
    Realm 모델 파일, Repository 패턴 코드들
  • Protocol
    화면전환과 같이 화면 하나에만 사용하는 프로토콜이 아니라 전역적으로 사용할 프로토콜
  • Utility
    BaseView, BaseViewController
  • Extension
    UIKit, Foundation 등을 확장해서 전역적으로 사용할 코드들 

 

각 화면 UI 작업

코드베이스로 진행 + BaseView/BaseViewController 를 사용했기 때문에 각 화면에서도 이를 적용하면 된다.

 

메모 앱 프로젝트의 경우 크게 화면이 3개로 분리되어 있었고

- 최초 팝업 화면

- 메모 리스트 화면 (= 여기서 검색화면도 다룬다.)

- 메모 작성하기/수정하기 (= 하나의 파일로 관리한다.)

이렇게 세가지 폴더를 나눠서 작업했다.

 

BaseView와 BaseViewController를 어떻게 활용하는가?!

한 화면만 간략하게 보자면, .. (코드가 제일 적은 WalkThrough 화면을 보도록 하겠음요)

import UIKit

import SnapKit
import Then

final class WalkThroughView: BaseView { ✅
    
    private var backView = UIView().then {
        $0.backgroundColor = .foreground
        $0.layer.cornerRadius = 10
    }
    
    private var label = UILabel().then {
        $0.text = """
                  처음 오셨군요!
                  환영합니다 :)
                  
                  당신만의 메모를 작성하고
                  관리해보세요!
                  """
        $0.textColor = .text
        $0.font = .systemFont(ofSize: 16, weight: .semibold)
        $0.numberOfLines = 0
    }
    
    var confirmButton = UIButton().then {
        $0.setTitle("확인", for: .normal)
        $0.setTitleColor(.text, for: .normal)
        $0.backgroundColor = .systemOrange
        $0.titleLabel?.font = .systemFont(ofSize: 18, weight: .bold)
        $0.layer.cornerRadius = 8
    }
    
    override func configureUI() {
        addSubview(backView)
        
        backView.addSubview(label)
        backView.addSubview(confirmButton)
    }
    
    override func setConstraints() {
        backView.snp.makeConstraints { make in
            make.centerX.centerY.equalToSuperview()
            make.width.equalTo(200)
            make.height.equalTo(230)
        }
        
        label.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview().inset(15)
            make.height.equalTo(130)
        }
        
        confirmButton.snp.makeConstraints { make in
            make.top.equalTo(label.snp.bottom).offset(8)
            make.leading.trailing.equalToSuperview().inset(15)
            make.height.equalTo(47)
        }
    }
}

이렇게 View에서 BaseView를 상속 받고

 

import UIKit

final class WalkThroughViewController: BaseViewController {

    // MARK: - UI Property
    
    private let walkThroughView = WalkThroughView()
    
    // MARK: - Life Cycle
    
    override func loadView() {
        self.view = walkThroughView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    // MARK: - Custom Method
    
    override func configure() {
        view.backgroundColor = UIColor.init(white: 0, alpha: 0.5)
        configureButton()
    }

    private func configureButton() {
        walkThroughView.confirmButton.addTarget(self, action: #selector(touchUpConfirmButton), for: .touchUpInside)
    }
    
    // MARK: - @objc
    
    @objc func touchUpConfirmButton() {
        UserDefaults.standard.set(true, forKey: Constant.UserDefaults.isNotFirst)
        dismiss(animated: true)
    }
}

ViewController는 BaseViewController를 상속 받아서 사용하면 된다. 

View를 갈아 끼우고 (loadView()) 

configure함수에서 UI 관련 코드를 작성한 뒤

이벤트 액션/인터랙션과 관련된 코드 및 비즈니스 코드를 작성하면 된다.

 

비슷한 방식으로 다른 화면들의 UI도 만들면 된다.

개인적으로 이전의 코드방식과 달랐던 부분은 View에서 Protocol 채택을 하지 않고 ViewController에서 하는 것이었다.

예를 들면, View에 UITableView가 있다면, UITableVIew Delegate / DataSource 등을 채택하게 되는데 이를 View에서 채택하는 것이 아니라 

    private func configureTableView() {
        listView.listTableView.delegate = self
        listView.listTableView.dataSource = self
    }

이렇게 작성하여 ViewController에서 채택한다.

(*왜 이렇게 하는가? View/ViewController의 역할을 잘 나누기 위해서)

 

기능 구현 (조건 처리)

각 화면별로 어떤 기능이 있는지 리스트 업을 하고 하나씩 구현하면 된다.

 

Walk Through

최초 팝업 화면의 경우는 큰 기능은 없고 최초 1회를 분기처리 했어야 했는데

이 화면으로 나오는 것이 아니라, 메모리스트(부모뷰) -> 팝업(자식뷰)의 흐름에서 나와야 하므로 로직처리를 여기서 하는 것이 아니라 ListViewController에서 처리해야 한다.

 

    private func presentWalkThrough() {
        let viewController = WalkThroughViewController()
        viewController.modalTransitionStyle = .coverVertical ✅ 
        viewController.modalPresentationStyle = .overFullScreen ✅
        present(viewController, animated: true)
    }

.overFullScreen으로 해야 뒤의 배경이 보이면서 팝업창이 나타나므로 위와 같이 스타일을 지정한 뒤,

 

    override func viewDidLoad() {
        super.viewDidLoad()
        if !UserDefaults.standard.bool(forKey: Constant.UserDefaults.isNotFirst) {
            presentWalkThrough() ✅
        } 
    }

UserDefaults로 최초 사용자인지 확인한 뒤 최초라면 메모리스트 화면에서 팝업 뷰로 화면을 띄운다.

 

그리고 팝업 뷰에서는,

    @objc func touchUpConfirmButton() {
        UserDefaults.standard.set(true, forKey: Constant.UserDefaults.isNotFirst)
        dismiss(animated: true)
    }

확인 버튼을 누르면서 UserDefaults 값을 변경하고 화면을 내리면 된다.

 


List

메모 리스트에서는 기능이 많기 때문에 차근 차근 진행해야 한다.

 

1. UITableView로 리스트 만들기

2. Realm으로 데이터 읽기

3. Realm 데이터 가공하기 

4. Leading/Trailing 기능 및 Tap 기능 구현

5. Search 기능 구현 

.. 이 순서로 진행했다. 

 

Write

마찬가지로 UI를 만들고 그 안에서의 기능을 구현했다.

 

생각할 조건 중 하나가 TextView에서 return key를 누르면 제목->내용으로 변경해야 하는데

.. 가지가지나물처럼 여러 방법을 해보다가 .. TextView를 2개 를 두고 (약간의 꼼수처럼) Responder를 바꿔가면서 구현했다.

 

TextView에서 처리할 분기처리는

  • 생성/수정일 때 분기처리 (생성이라면 바로 편집 상태, 수정이라면 탭했을 때 편집 상태)
  • TextView를 2개로 두어 TitleTextView, ContentTextView 로 나눴기 때문에 이에 대한 로직 처리 
  • TitleTextView의 영역이 늘어남에 따라서 동적으로 높이가 변할 수 있도록 로직 처리 

.. 등이 있다.

// MARK: - UITextView Protocol

extension WriteViewController: UITextViewDelegate {
    func textViewDidBeginEditing(_ textView: UITextView) {
        showNavigationItem() 1️⃣ 
    }
    
    func textViewDidChange(_ textView: UITextView) {
        let size = CGSize(width: view.frame.width, height: .infinity)
        let estimatedSize = writeView.titleTextView.sizeThatFits(size)
        writeView.titleTextView.constraints.forEach { (constraint) in
            if constraint.firstAttribute == .height {
                constraint.constant = estimatedSize.height 2️⃣ 
            }
        }
    }
    
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        if text == "\n" && textView == writeView.titleTextView { 3️⃣
            textView.resignFirstResponder()
            writeView.contentTextView.becomeFirstResponder() 
        }
        
        if let char = text.cString(using: String.Encoding.utf8) {
            let isBackSpace = strcmp(char, "\\b") 4️⃣
            if (isBackSpace == -92) {
                if writeView.contentTextView.text == "" {
                    let newPosition = writeView.titleTextView.endOfDocument
                    writeView.titleTextView.selectedTextRange = writeView.titleTextView.textRange(from: newPosition, to: newPosition)
                    
                    writeView.titleTextView.becomeFirstResponder()
                    writeView.contentTextView.resignFirstResponder()
                }
            }
        }
        
        return true
    }
}

1️⃣ 편집 상태라면 ToolBar가 보이도록

2️⃣ TitleTextView의 높이가 동적으로 변하도록

3️⃣ 리턴 키를 눌렀을 때 Title -> Content TextView로 responder 이동

4️⃣ 백스페이스 키를 눌렀을 때 Content TextView에 커서가 있다면 Title TextView로 이동 

 

 

작성/수정이 같은 화면을 사용하므로 작성상태인지, 수정상태인지 알려주는 변수를 두어 분기처리를 했다.

    var isNew: Bool = true {
        didSet {
            if isNew { ✅
                writeView.titleTextView.becomeFirstResponder()
                showNavigationItem()
            } else {
                navigationItem.rightBarButtonItems = nil
            }
        }
    }
    
    ...
    
    private func backToListView(_ isNew: Bool) { 
        if isNew && isDoneButtonTapped == false { ✅
            if writeView.titleTextView.hasText {
                let task = Memo(memoTitle: writeView.titleTextView.text, memoContent: writeView.contentTextView.text, memoDate: Date())
                repository.addItem(item: task)
            }
        } else {
            if writeView.titleTextView.text == "" {
                repository.deleteItem(item: memo)
            } else {
                repository.updateItem(value: ["objectId": memo.objectId,
                                              "memoTitle" : writeView.titleTextView.text,
                                              "memoContent" : writeView.contentTextView.text])
            }
        }
    }
    
    ...
    
    @objc func touchUpDoneButton() {
        isDoneButtonTapped = true
        if isNew && writeView.titleTextView.hasText {
            let task = Memo(memoTitle: writeView.titleTextView.text, memoContent: writeView.contentTextView.text, memoDate: Date())
            repository.addItem(item: task)
        } else {
            if writeView.titleTextView.hasText {
                repository.updateItem(value: ["objectId": memo.objectId,
                                              "memoTitle" : writeView.titleTextView.text,
                                              "memoContent" : writeView.contentTextView.text])
            } else {
                repository.deleteItem(item: memo)
            }
            
        }
        navigationController?.popViewController(animated: true)
    }

> 작성인지, 수정인지에 따라서 UI도 바뀌고

> 뒤로가기 를 했을 때의 로직 처리도 달라지고 (생성인지/수정인지 등 ..)

> 완료 버튼을 눌렀을 때 로직 처리도 달라진다. 

 

_

모든 기능에 대한 코드를 적기엔 .. 제법 많고 ..

사실 대부분이 위기였기 때문에 .. 이후의 과정은 .. [위기]글을 봐주세요 >< 

 

(근데 나도 위기로 이어지고 싶지 않았음.)