- 프로젝트 기간 : 2021.08.30 - 09.17
- 개인프로젝트
- grounds rules
- 10시에 스크럼 시작
- 프로젝트가 중심이 아닌 학습과 이유에 초점을 맞추기
- 의문점을 그냥 넘어가지 않기
- 커밋규칙
- 브랜치 : main → 3-tacocat → step1
- 카르마스타일
- 메서드 및 타입단위로
Dependency Manager
(SwiftLint, CocoaPods, SPM, 카르타고)Compact&Regular Size
SplitViewController
CoreData
Dynamic Type
Accessibility
iPad and iPhone Traits
Data Transfer between ViewControllers
Singleton
Dependency Injection
Swift Performance
1. CRUD (Use Core Data)
2. Adapt LayoutTraits through SplivtViewController
3. Implement UI Element Programmatically
4. Dependency Manager
5. Accessibility
6. Cell Swipe
7. Alert
🌟구현기능🌟
-
CRUD에 참여하는 객체와 이벤트에 따른 정보의 흐름
ReadUpdate CreateDelete 코어데이터에 관하여
Reference -
Core data Stack = 영구저장소 + 오브젝트 모델 + 영구저장소 코디네이터 + 메니지드 오브젝트 컨텍스트
-
대부분은 컨텍스트가 제공하는 API 로 기능을 구현
- hasChanges, save, fetch 등등
-
컨텍스트를 통해 필요한 정보를 저장 → 영구저장소 Coordinator 가 컨테이너와 모델 사이를 중계 , 오브젝트 모델을 통해 구조 파악 → 영구저장소에 알아서 저장
-
persisTent Store
- 4가지, 기본은 SQLite(non aomic store)
- xml, binary는거의 안 쓰고 In-Memory는 캐쉬 할 때 사용하기도 한다
-
Object Model
NSManagedObjectModel
객체 이용- 코드를 통해 직접 구성할 수도 있지만 xCode통해 많이 구현 함
-
Persistent Store Coordinator
NSPersistentStoreCoordinator
객체로 만든다- 컨테이너와 모델 사이 저장을 할 수 있도록 중계
- 모델과 컨텍스트의 참조를 유지시켜준다.
- 영구저장소를 관리한다.
-
Managed Object Context
NSManagedObjectContext
객체로 만든다.- 코어데이터에서 데이터 만들고 컨텍스트에서 저장을 요청(임시저장)
- 여기에 저장안하고 끄면 모두 사라짐
- 영구저장소에서 데이터 가져와서 처리하는 곳도 컨텍스트. 그 땐 복사본을 가져온다
- 컨텍스트의 데이터를 수정해도 원본은 수정되지 않는다.
-
-
Persistent Container
- 코어데이터 스택을 하나의 개념으로 추상화 한 것
- 실제 코드
- 앱에 기본적으로 구현되는 것
// MARK: - Core Data stack lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "CoreDataTutorial") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container }() // MARK: - Core Data Saving support func saveContext () { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } catch { let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } }
-
초기화 순서
- 모든 컨텍스트 객체가 코디네이터에 대한 참조값을 가지는 것은 아니다.
리팩토링 전 코드
MemoDataManager
라는 타입을 만들어서 model object Context 를 배열로 관리하도록 하였다.
//MemoDataManager.swift final class MemoDataManager { static var memos: [Memo] = { () -> [Memo] in do { let test = try context.fetch(Memo.fetchRequest()) as [Memo] return test } catch { return [] } }() static let context: NSManagedObjectContext = { () -> NSManagedObjectContext in guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) } let context = appDelegate.persistentContainer.viewContext return context }() }
- CoreDataAccessible 프로토콜을 통해 코어데이터의 context의 여러 메소드를 캡슐화 하였다.
//CoreDataAccessible.swift protocol CoreDataAccessible { func fetchCoreDataItems( context: NSManagedObjectContext, tableview: UITableView) func saveCoreData( context: NSManagedObjectContext) func deleteCoreData( context: NSManagedObjectContext, deletedObject: NSManagedObject) } extension CoreDataAccessible { func fetchCoreDataItems( context: NSManagedObjectContext, tableview: UITableView) { do { MemoDataManager.memos = try context.fetch(Memo.fetchRequest()) DispatchQueue.main.async { tableview.reloadData() } } catch { print(CoreDataError.fetchError.localizedDescription) } } func saveCoreData( context: NSManagedObjectContext) { do { try context.save() } catch { print(CoreDataError.saveError.localizedDescription) } } func deleteCoreData( context: NSManagedObjectContext, deletedObject: NSManagedObject) { context.delete(deletedObject) } func deleteSaveFetchData( context: NSManagedObjectContext, deletedObject: Memo, tableView: UITableView) { deleteCoreData(context, deletedObject) saveCoreData(context) fetchCoreDataItems(context, tableView) } }
-
-
리팩토링 후 코드(Trouble Shooting에 리팩토리 과정에 대해 자세히 기재하였다)
리팩토링 후 코드
final class CoreDataManager { static let shared = CoreDataManager() private init() { } lazy var memos: [Memo] = { () -> [Memo] in do { let test = try context.fetch(Memo.fetchRequest()) as [Memo] return test } catch { return [] } }() private let context: NSManagedObjectContext = { () -> NSManagedObjectContext in guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) } let context = appDelegate.persistentContainer.viewContext return context }() func updateMemo(content: String, location: Int, completionHandler: @escaping () -> Void) { let titleAndBody = content.seperateTitleAndBody() let title = titleAndBody.title let body = titleAndBody.body let targetMemo = CoreDataManager.shared.memos[location] targetMemo.title = title targetMemo.body = body saveCoreData() fetchCoreDataItems() completionHandler() } func createMemo(completionHandler: @escaping () -> ()) { let newMemo = Memo(context: CoreDataManager.shared.context) newMemo.lastModifiedDate = Date(timeIntervalSince1970: Date().timeIntervalSince1970) self.memos.append(newMemo) self.saveCoreData() self.fetchCoreDataItems() completionHandler() } func deletMemo( deletedMemoIndex: Int, completionHandler: @escaping () -> ()) { if deletedMemoIndex < .zero { return } let deletedMemo = self.memos[deletedMemoIndex] self.context.delete(deletedMemo) saveCoreData() fetchCoreDataItems() completionHandler() } private func fetchCoreDataItems() { do { self.memos = try self.context.fetch(Memo.fetchRequest()) } catch { print(CoreDataError.fetchError.localizedDescription) } } private func saveCoreData() { do { try self.context.save() } catch { print(CoreDataError.saveError.localizedDescription) } } } ```
-
만약 아이폰, 아이패드 두가지 기기에서 동시에 제품이 사용되는 경우 중점을 두어야 하는 부분은 무엇일까? ->
Traits
,UI/UX
Traits
:Application
이 실행 되는 환경LayoutTraits
: SizeClass, Dynamic Type, Layout DirectionAppearance Trits
: Display Gamut, 3D Touch, Dark/Light Mode
- UI/UX란?
UI를 통해 제품이 제공하고자 하는 UX를 만들어 나가는 것
- UI : User Interface, 사용자가 product와 interact하는 환경 및 요소
- 예를들어 사용자의 touch, dragging, swipe 등
User experience is determined by how easy or difficult it is to interact with the user interface elements that the UI designers have created.
- UX : User Experence, 사용자가 제품 혹은 서비스를 이용하면서 느끼는 경험 (참고영상 : These Are The 5 Big Differences Between UX And UI Design)
- UI : User Interface, 사용자가 product와 interact하는 환경 및 요소
-
현 프로젝트에선
LayoutTraits
에 중심을 두었다.SplitViewController
를 이용해서 SizeClass, 기기의 방향 등의 Layout Traits가 달라질 때 마다 레이아웃, 화면에 보여지는 형식 등이 결정되도록 구현했다.- 이유 :
SplitViewController
는 컨텐츠 계층을 보여주는 가장 최상위 레벨로서 2~3개의 컬럼을 가지고 있으며 상위 컬럼이 변경되는 경우 그 계층에 속한 객체들도 같이 영향을 받기 때문에 (SplitView H.I.G)
- 이유 :
리팩토링 전 코드
final class SplitViewController: UISplitViewController { private let detailVC = MemoDetailViewController() private let primaryVC = MemoListViewController() override func viewDidLoad() { super.viewDidLoad() self.decideSpliveVCPreferences() self.delegate = self } } extension SplitViewController: UISplitViewControllerDelegate { func splitViewController( svc: UISplitViewController, topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column) -> UISplitViewController.Column { return .primary } func splitViewController( svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) { svc.presentsWithGesture = false } } extension SplitViewController { private func decideSpliveVCPreferences() { self.preferredDisplayMode = .oneBesideSecondary self.preferredSplitBehavior = .displace self.setViewController(primaryVC, for: .primary) self.setViewController(detailVC, for: .secondary) } } ```
final
키워드 붙인 이유 : Dynamic Dispatch대신 static Dispatch가 진행되어 run time 시에 더 빠른 속도로 실행하기 위하여presentsWithGesture
를 false로 한 이유 : 기능 명세서에서secondary
컬럼이 Regular Size width일 땐primary
컬럼과 같이 화면에 동시에 보여야해서prefferedDisplayMode
를oneBesideSecondary
로 할당하였다. 하지만 해당 메소드가 true인 경우 스플릿 뷰의display
모드를automatic
으로 변경하기 때문에 false로 할당하였다.
-
리팩토링 후 코드
-
UIView 클래스에서 extension을 통해 view의 위치를
setPosition
메소드에 알맞은 인자를 넣어서 계산하도록 구현extension UIView { func setPosition(top: NSLayoutYAxisAnchor?, topConstant: CGFloat = .zero, bottom: NSLayoutYAxisAnchor?, bottomConstant: CGFloat = .zero, leading: NSLayoutXAxisAnchor?, leadingConstant: CGFloat = .zero, trailing: NSLayoutXAxisAnchor?, trailingConstant: CGFloat = .zero) { translatesAutoresizingMaskIntoConstraints = false top.flatMap { topAnchor.constraint(equalTo: $0, constant: topConstant).isActive = true } bottom.flatMap { bottomAnchor.constraint(equalTo: $0, constant: bottomConstant).isActive = true } leading.flatMap { leadingAnchor.constraint(equalTo: $0, constant: leadingConstant).isActive = true } trailing.flatMap { trailingAnchor.constraint(equalTo: $0, constant: trailingConstant).isActive = true } } }
-
SPM을 이용해 SwiftLint추가하려 했으나 아직 SPM에서 지원하지 않았다. 그래서 CocoaPod으로 시도하였다.
// 추가한 SwiftLint Rules disabledrules: - linelength - trailingwhitespace - commentspacing - mark - colon - unusedclosureparameter
-
아래는 의존성 관리 도구의 종류와 각각의 특징이다.
의존성 관리도구
1. 의존성(dependency)라는 것은 외부의 독립적인 프로그램 모듈(라이브러리, 파일 혹은 여러개의 파일, 폴더, 특정작업이 가능한 패키지 등)을 의미하는 것. 그렇다면 의존성 관리도구라는 것은 이러한 것들을 관리하는 도구를 의미 2. Swift에선 Swift Package Manager(SPM)라는 의존성 관리도구를 제공 3. 의존성 관리도구를 사용하는 이유 i. 개발 환경에 알맞은 버전으로 의존성을 관리 ii. 가장 최신 것으로 일괄 업데이트 가능-
SPM 외의 의존성 관리도구와 특징
Cocoa pod
-
빌드할 때마다 패키지를 같이 빌드하기 때문에 길어짐
<장점>
carthage
-
코코아팟의 단점을 보완해서 등장, 프레임워크를 추가 <장점>
- 빌드속도가 빠름, 맨 처음에 프레임워크 만들 때 빌드 이미 함
- 어떤 오픈소스를 쓰는지 보기 편함
- 버전, 종속성 관리
<단점>
- 새로운 패키지 쓸 때마다 프레임워크 추가해줘야함
<장점> - 버전에 따라 빌드가 캐싱된다 - 같은 패키지라도 상황에 따라 버전 별로 사용할 수 있다 <단점> - 내부적으로 SPM을 사용하는 의존성 관리 도구. [출처](https://yagom.net/courses/open-source-library/lessons/코코아팟-vs-카르타고-vs-스위프트-패키지-매니저-2/)
spm
: 스위프트 패키지 매니저
-
2017년 11월에 release <장점>
- 애플이 지원한다. 👍
- Dynamic, Static 라이브러리를 모두 지원한다. (4.0 버전 이상)
- 의존성의 의존성까지 자동으로 관리해준다.
- 누구나 쉽게 어떤 의존성이 애플리케이션에 있는지 알 수 있다.
- 스위프트 언어에 built-in 되어있어 별다른 설치가 필요없다. (
Swift 3
이상) - 스위프트 언어에 built-in 되었기 때문에 Xcode Project 파일이 꼭 필요한 것이 아니므로 리눅스에서도 사용할 수 있다.
- Package.swift 파일 이외에 수행할 설정이 없다.
- Xcode의 GUI 환경에서 관리가 가능하다 (11.0 버전 이상)
- 내부 코드를 확인가능하다.
<단점>
- SPM에서 지원하지 않는 라이브러리가 있어, 사용하고자 하는 라이브러리의 지원 여부 확인 필수
- 탈중앙화 되어있기 때문에 라이브러리를 찾는 것이 더욱 수고스러울 수 있다
- 해결되지 않은 버그가 많다.
-
의존성 관리 도구 비교
- 빌드속도: Carthage > SPM ≥ CocoaPod
- 지원하는 라이브러리 수 : CocoaPod > SPM, Carthage
- 의존성 관리도구와 Git을 함께 사용할 때 주의할 점은?
- 라이브러리 설치 파일까지 Git 레포에 올라가지 않도록 주의
- gitignore를 작성하기 전에 지워야하는 파일을 삭제하고 리모트에 반영한 뒤 gitignore를 다시 작성
-
-
글씨에
dynamic Size
를 적용할 수 있는 코드를 관련 UI요소에 구현하여 다이나믹 사이즈가 적용되도록 하였다.//MemoListTableViewCell.swift private func setLabelStyle() { self.setDynamicType(titleLabel, .title3) self.setDynamicType(dateLabel, .body) self.setDynamicType(bodyLabel, .caption1) self.titleLabel.textAlignment = .left self.bodyLabel.textColor = .gray } private func setDynamicType( label: UILabel, font: UIFont.TextStyle) { label.adjustsFontForContentSizeCategory = true label.font = UIFont.preferredFont(forTextStyle: font) }
- 테이블 뷰의 cell을 swipe 할 때 Delete, Share의 옵션을 가지도록 구현하였다.(share 기능은 구현하지 못하고 프로젝트가 종료되었다)
extension MemoListViewController: UITableViewDelegate { // .... func tableView( tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let deleteAction = UIContextualAction( style: .destructive, title: SelectOptions.delete.message, handler: { [self] , , in if let splitViewController = self.splitViewController as? SplitViewController { let deletedMemoIndex = indexPath.row splitViewController.delete(deletedMemoIndex) } }) let shareAction = UIContextualAction( style: .normal, title: SelectOptions.share.message, handler: { action, view, completionHandler in print("share action구현하기 ") }) let swipeActions = [deleteAction, shareAction] return UISwipeActionsConfiguration(actions: swipeActions) } }
seeMore
버튼을 클릭했을 때 3개의 선태 메뉴가 나오도록 구현하였다. (share 기능은 구현하지 못하고 프로젝트가 종료되었다)extension MemoDetailViewController { @objc func didTapSeeMoreButton() { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let deleteActions = UIAlertAction(title: SelectOptions.delete.message, style: .destructive, handler: { [self] action in let deletAlert = UIAlertController(title: "진짜요?", message: "정말로 삭제하시겠어요?", preferredStyle: .alert) let deleteAlertConfirmAction = UIAlertAction(title: "삭제", style: .destructive) { action in if let splitViewcontroller = self.splitViewController as? SplitViewController { splitViewcontroller.delete(self.memoIndex) self.memoIndex -= 1 self.textView.text = "" } } let deleteAlertCancelAction = UIAlertAction(title: "취소", style: .cancel) deletAlert.addAction(deleteAlertCancelAction) deletAlert.addAction(deleteAlertConfirmAction) self.present(deletAlert, animated: true, completion: nil) }) let shareAction = UIAlertAction(title: SelectOptions.share.message, style: .default, handler: { action in }) let cancleAction = UIAlertAction(title: SelectOptions.cancle.message, style: .cancel, handler: { action in }) alert.addAction(shareAction) alert.addAction(deleteActions) alert.addAction(cancleAction) present(alert, animated: true, completion: nil) } }
-
해결 방향
:SplitViewConroller
(이하splitVC
)에서 자신이 가지고 있는primary
,secondary
column에서 일어나는 이벤트에 대한 메세지를 처리하는 방향으로 구현하였다.- 이유 : 애플에서 아래와 같이
splitVC
에서chilVC
의 message들은splitVC
을 거치는 흐름으로 구현하라고 권고 하였으며ViewController
사이에서 직접적으로 데이터를 전달하는 것 보다 훨씬 더 Decoupled 한 구조를 가지 수 있다. 또한 협업 시 모든 뷰컨을 알고있을 필요가 줄어든다.Message Forwarding A split view controller interposes itself between the app’s window and its child view controllers. As a result, all messages to the child view controllers must flow through the split view controller. Messages are forwarded as appropriate. For example, view appearance and disappearance messages are sent only when the corresponding child view controller actually appears onscreen.
- 이유 : 애플에서 아래와 같이
-
리팩토링 코드
extension SplitViewController { func presentMemo(location: Int) { let memo = selectedMemo(location) self.detailViewController.presentMemo(memo, location) self.show(.secondary) } private func selectedMemo( index: Int) -> Memo { CoreDataManager.shared.memos[index] } func delete( memo: Int) { CoreDataManager.shared.deletMemo(memo) { self.primaryViewController.updateTableView() } self.show(.primary) } func creatNewMemo( location: Int) { CoreDataManager.shared.createMemo { self.primaryViewController.updateTableView() } let memo = CoreDataManager.shared.memos[location] self.detailViewController.presentMemo(memo, location) self.show(.secondary) } func updateMemo( memo: String, location: Int) { CoreDataManager.shared.updateMemo(content: memo, location: location) { self.primaryViewController.updateTableView() } } }
//TableView가 있는 ViewController extension MemoListViewController: UITableViewDelegate { func tableView( tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let memoIndex = indexPath.row if let splitViewController = self.splitViewController as? SplitViewController { splitViewController.presentMemo(location: memoIndex) } self.tableView.deselectRow(at: indexPath, animated: true) } //.. } //TextView가 있는 ViewController extension MemoDetailViewController: UITextViewDelegate { func textViewDidChange( textView: UITextView) { let changedMemo = self.textView.text ?? placeHolder if let splitViewController = self.splitViewController as? SplitViewController { splitViewController.updateMemo(changedMemo, self.memoIndex) } } //.. }
상황 1-2. 데이터 전달 과정에서 MemoListVC
, MemoDetailVC
가 tableView
, indexPath
와 연관 및 의존관계를 형성하고 있다. 또한 CoreData를 여러 ViewController에서 접근하고 있어서 thread safe하지 않은 CoreData의 특성으로 인해 오류가 생기고 있다.
-
해결방향
: 코어데이터를Singleton
객체로 만들어서splitVC
에서만 데이터 접근하도록 하였다. -
리팩토링 코드
final class CoreDataManager { static let shared = CoreDataManager() private init() { } lazy var memos: [Memo] = { () -> [Memo] in do { let test = try context.fetch(Memo.fetchRequest()) as [Memo] return test } catch { return [] } }() private let context: NSManagedObjectContext = { () -> NSManagedObjectContext in guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) } let context = appDelegate.persistentContainer.viewContext return context }() func updateMemo(content: String, location: Int, completionHandler: @escaping () -> ()) { let titleAndBody = content.seperateTitleAndBody() let title = titleAndBody.title let body = titleAndBody.body let targetMemo = CoreDataManager.shared.memos[location] targetMemo.title = title targetMemo.body = body saveCoreData() fetchCoreDataItems() completionHandler() } func createMemo(completionHandler: @escaping () -> ()) { let newMemo = Memo(context: CoreDataManager.shared.context) newMemo.lastModifiedDate = Date(timeIntervalSince1970: Date().timeIntervalSince1970) self.memos.append(newMemo) self.saveCoreData() self.fetchCoreDataItems() completionHandler() } func deletMemo( deletedMemoIndex: Int, completionHandler: @escaping () -> ()) { if deletedMemoIndex < .zero { return } let deletedMemo = self.memos[deletedMemoIndex] self.context.delete(deletedMemo) saveCoreData() fetchCoreDataItems() completionHandler() } private func fetchCoreDataItems() { do { self.memos = try self.context.fetch(Memo.fetchRequest()) } catch { print(CoreDataError.fetchError.localizedDescription) } } private func saveCoreData() { do { try self.context.save() } catch { print(CoreDataError.saveError.localizedDescription) } } }
리팩토링 전 | 리팩토링 후 |
---|---|
경고 메세지 | Debugging Consol |
---|---|
textView에서 body에 해당하는 부분 입력 시 왼쪽 테이블뷰 cell의 레이아웃이 변경 | |
해결
: 스택뷰가topAnchor
로 가지고 있던titleLabel
의bottomAnchor
에nil
이 아닌contentView
의 anchor를 할당- 이전 코드
//MainTableViewCell titleLabel.setPosition(top: nil, bottom: nil, leading: safeAreaLayoutGuide.leadingAnchor, leadingConstant: 10, trailing: contentView.trailingAnchor)
- 수정 후 코드
//MainTableViewCell titleLabel.setPosition(top: contentView.topAnchor, bottom: contentView.bottomAnchor, bottomConstant: -20, leading: contentView.leadingAnchor, trailing: contentView.trailingAnchor) // 밑에서 설명하겠지만 Hugging, Compression priority까지 같이 구현 하였음
- 이전 코드
결론
: cell 내부 UI요소들의 오토레이아웃이 잘 구현되지 않아서 생겼던 문제. 다양한 변화에 대응할 수 있는 autolayout을 구현해야 UI요소와 그 내부 컨텐츠들이 화면에 나타날 수 있다.
-
시도1
. cell의 높이가 dynamic하게 resizing 되지 않아 겹치는 것일 수 있기 때문에PrimaryViewController
에 아래 코드 추가 -> 변화없음tableView.rowHeight = UITableView.automaticDimension
-
시도2
. top과 bottom anchor 를 지정 -> 이전보다 나아짐private func setupTitleLabelLayout() { // dateLabel, bodyLabel초기화 및 view에 추가 //... let height = 30 dateAndBodyStackView.setPosition(top: contentView.topAnchor, topConstant: height, bottom: contentView.bottomAnchor, leading: contentView.leadingAnchor, trailing: contentView.trailingAnchor) titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) }
- 결과
-
결론
- 객체와 객체사이의 수직 간격이 이전에는 없었기 때문에 간격을 넓힘으로서 위 아래 레이블이 완전히 겹치는 문제는 해결되었다.
Dynamic Size
가 커치면서stackView
에 넣은dateLabel
,BodyLabel
이 잘 안보이는 부분은 무엇때문일까? -> cell의 높이가 동적으로 변하도록시도1
의 코드를 추가했음에도 글씨크기 변경해 주었을 때 cell의 높이가 변경되지 않으므로 cell의 높이에도 어느정도의 제한이 있는 것으로 관찰된다.//시도1 tableView.rowHeight = UITableView.automaticDimension
-
지금처럼 간격을 지정하는 방법이 아닌 사이즈에 따라 알아서 높이가 지정되도록 하는 방법은 무엇이 있을까?
-
코드 수정 전 코드 수정 후 - 수정전 코드
private func makeHorizontalStackVeiw() { dateAndSubStackView = UIStackView(arrangedSubviews: [self.dateLabel, self.subTitleLabel]) contentView.addSubview(dateAndSubStackView) dateAndSubStackView.translatesAutoresizingMaskIntoConstraints = false dateAndSubStackView.axis = .horizontal dateAndSubStackView.distribution = .equalCentering dateAndSubStackView.spacing = 40 dateAndSubStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true dateAndSubStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true dateAndSubStackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true }
- 수정 후 코드 : leading 부분만 safeAreaLayoutGuide로 수정
dateAndSubStackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor).isActive = true
-
이유
- 여러가지 이유가 있겠지만 아래와 같은 이유로 처음에 레이아웃이 잘 잡히지 않은 듯 하다.
-
bodyLabel
의compressionResistence
가dateLabel
과 동일거나 높아서bodyLabel
의 내부 컨텐츠의 내용이 많아지자dateLabel
이 축소됨. -
공식문서에 보면 아래와 같이 나와있다.
Your app’s content occupies most of the cell’s bounds, but the cell may adjust that space to make room for other content.
즉 cell은 내부의content를 잘 표시하기 위해 cell의 bounds를 적절하게 변경할 수 있다. 따라서 leading anchor를 contentView로 정했을 때 추가적인 정보가 없다면 cell의 입장에선 어떤 것을 우선순위로 제한된 cell 내부에 보여야할지 모르게 된다.
-
실험
: contentView로 레이아웃을 잡을 때 아래와 같이 구현하면 같은 결과가 나온다.
dateAndBodyStackView.setPosition( top: titleLabel.bottomAnchor, bottom: contentView.bottomAnchor, leading: contentView.leadingAnchor, trailing: contentView.trailingAnchor ) dateLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
- 여러가지 이유가 있겠지만 아래와 같은 이유로 처음에 레이아웃이 잘 잡히지 않은 듯 하다.
-
iPad pro / iphone11 에서의 모습
-
시도1
:cellLayoutMarginsFollowReadableWidth
를 true로 설정해서 그런것이라고 판단하고 원래의 default 로 변경 -> 변화없음- 이유 : 위의 속성은 cell이 default 스타일 중 하나일때만 자동으로 여백이 조정되도록 하는 속성이기 때문
-
시도2
: tablView의 레이아웃을 safeAreaLayoutGuide 를 기준으로 리팩토링// 여기서 setAcnchor는 추후 setPosition으로 reNaming tableView.setAnchor(top: view.safeAreaLayoutGuide.topAnchor, bottom: view.safeAreaLayoutGuide.bottomAnchor, leading: view.safeAreaLayoutGuide.leadingAnchor, trailing: view.safeAreaLayoutGuide.trailingAnchor)
- 결과
-
시도3
: 테이블뷰가 전체 화면을 다 채우도록 구현tableView.frame = view.bounds
-
결론
: tableView의 레이아웃이 잘 잡히지 않아 생겼던 문제. 시도2, 시도3의 방법으로 구현하니 정상적으로 잘 나왔다.cellLayoutMarginsFollowReadableWidth
속성은 custom cell에선 영향이 없다.
1. cell에게 정보를 전달하는 방법
2. 어떻게하면 성능을 좋게 할 수 있을까
3. ViewController의 역할 최대한 분리하는 방법
4. 코어데이터 저장소는 어떤 객체가 가지고 있어야 할까
5. TextView에서 제목과 본문을 나눠야 하는데 어떻게 나눌 수 있을까
6. 코어데이터의 entity, attribute의 구성
7. MVC모델에 기반한 grouping
8. lazy키워드, closure를 이용해 UI요소를 초기화 하면 좋은 부분은 어디일까
9. CellId
열거형
🌟고민한 부분🌟
- 아래와 같이 Holder에 담아서 전달한다.
- 이유 : Holder내부에서 데이터 가공 할 수 있으며 이 역할을 Holder에게 부여하므로서 각 객체의 역할을 확실히 구분할 수 있기 때문이다.
final class CellContentDataHolder {
let titleLabelText: String
let dateLabelText: String
let bodyLabelText: String
init(title: String, date: Date, body: String) {
let modifiedDate = DateFormatter().updateLastModifiedDate(date)
self.dateLabelText = "\(modifiedDate)"
self.titleLabelText = title
self.bodyLabelText = body
}
}
}
-
class타입의 참조 기능만 사용하는 경우
final
키워드 추가 한다- 이유 : Dynamic Dispatch가 아닌 Static Dispatch로 메서드 디스패치의 방법을 바꿀 수 있기 때문
- 현재 사용되는 대부분의 class타입의 객에에 final 키워드 추가 하였다.
-
구조체 내부에 속성의 타입이 참조타입이 많은 경우 구조체가 아닌 클래스로 바꾼다.
- 이유 : 구조체의 경우 copy 가 일어날 때 속성의 heat영역도 copy 되기 때문에 class로 구현했을 때보다 Reference Count 오버헤드가 발생한다.
- cellDataHolder객체를 struct에서 class로 변경
final class CellContentDataHolder { let titleLabelText: String let dateLabelText: String let bodyLabelText: String init(title: String, date: Date, body: String) { let modifiedDate = DateFormatter().updateLastModifiedDate(date) self.dateLabelText = "\(modifiedDate)" self.titleLabelText = title self.bodyLabelText = body } }
1-1. MemoListViewController가 TableViewDataSource 역할을 하지않고 customDataSource를 따로 구현하였다.
-
코드
final class TableViewDataSource: NSObject, UITableViewDataSource { static let identifier = "cell" func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return CoreDataManager.shared.memos.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: MemoListTableViewCell.identifier, for: indexPath) as? MemoListTableViewCell else { return UITableViewCell() } let memo = CoreDataManager.shared.memos[indexPath.row] let cellContent = CellContentDataHolder(title: memo.title ?? "", date: memo.lastModifiedDate ?? Date(), body: memo.body ?? "") cell.configure(cellContent) return cell }
-
결론
: 모든 상황에 항상 적용되는 패턴, 코드라는 것은 없다. -
생각의 흐름
- 'MVC 패턴에서 ViewController의 역할이 무엇인가'
- Controller의 역할은 뷰에 내용을 주는
presenter
의 역할을 하면서도 비지니스 로직이(Model
) 앱의 필요 기능을 위해 배치되는 곳이라고 생각했다.- 마치 레고(Model)를 가지고 사람, 성, 마을, 나라 등(Application) 을 만들 수 있는 것 처럼
- 이와 같은 이유로 ViewController가 TableView의 DataSource의 역할을 하는 것은 맞지 않다고 생각했다.
- 하지만
UIKit에서 제공해주는 delegate에 더 필요한 부분이 있거나 override해야하거나 한다면 이점
이 있는게 아니라면 굳이 두 번 거칠 필요없이 해당 컨트롤러에 delegate역할을 부여하는 것이 오히려 더 생산적인 방향일 수 있다.
-
delegate, datasource등을 분리하는것을 모든 상황에 적용해야하는 법칙이 아니다.
-
후보
- 지금은 기능 구현에 초점을 맞추고 Primary VC에 두었는데 추후 타입을 하나 만들어 들고있게 하거나 프로토콜로 만들 수 있지 않을까?
- 코어 데이터가지는 프로토콜 하나 만들면 container도 extension으로 구현해 두고 fetch 하는 메소드도 가직 있도록 할 수 있을 것 같다.
-
선택한 방향
: SplitVC가 MemoListVC, MemoDetailVC를 알고있기 때문에 CoreDataManager 객체를 만들어 이 객체가 데이터를 가지고 있으면서 SplitVC가 이 객체 접근하는 방식으로 최종 구현
- 아래와 같이 TextView의 text 속성을 분리하여 튜플로 반환하는 extension메소드를 사용해 보았다.
extension String {
func seperateTitleAndBody() -> (title: String, body: String) {
let lineBreaker = "\n"
let emptyTtitle = ""
let bodyStartIndex = 1
let seperateArrray = self.components(separatedBy: lineBreaker)
let title = seperateArrray.first ?? emptyTtitle
let body = seperateArrray[bodyStartIndex...].reduce("") { $0 + lineBreaker + $1 }
return (title, body)
}
}
- 메모의 title, body, date를 따로 나눠서 저장하는 방법과 통째로 저장하고 추후 필요할 때 가공해서 보여주는 방법 중 어떤것을 선택해야할까?
결론
- 현재는 두 방법의 차이가 크게 없을 것 같아서 구분하여 넣어주는 방법을 선택했다. 추후 프로젝트가 커지거나 프로젝트의 흐름이 바뀐다면 다시 한 번 고민해보면 좋을 것 같다.
-
Model
의 기준 :ViewController
에서 인스턴스로 만들어지거나 메소드의 매게변수, 내부 지역변수 등으로 사용되는가 -
View
의 기준 : 사용자에게 보여지는 UI요소와 관련되어 있는가 -
Controller
의 기준 :View
와Model
의 상호작용이 구현되어있는가
- Closure
- 해당 UI요소 자체의 속성을 다양하게 정해주어야 하는 경우
- 구체적인 속성을 초기화 할 때 정할 수 있다는 장점이 있다.
- lazy 키워드
- 해당 키워드를 붙이는 상황
- 사용자 interaction에 따라 생성 여부가 결정될 때
- 화면에 로드를 빠르게 해주고 싶을 때
- 해당 키워드를 붙이는 상황
- 프로젝트에서 사용한 부분
//MemoDetailViewController
private lazy var textView = UITextView()
//MemoListViewController
private lazy var tableView = UITableView()
- cell identifier를 cell 타입 내부에서 private 속성으로 가지는 방법과 CellID라는 enum을 구현하는 방법 중 어떤방법이 더 코드의 가독성, SOLID 측면에서 좋을까
결론
: cell identifier라는 건 결국 cell의 이름이기 때문에 cell 내부에 property로가지고 있는것이 좋을 것 같다고 생각하였다.
- 3주간 배운 내용을 정리해 보았다. 노션 링크
-
코어데이터의 에러처리
-
Nested Stack View
구현하지 못했던 부분 -
MemoDetailViewController
의textViewDidChange
메소드에서 텍스트뷰의text
속성 값이 변경 될 때마다 메모를 저장하고 있는 것 -
메모를 저장할 때 user default 가 아닌 keychain에 저장하도록 구현하면 더 안전한 저장 방식이 될 수 있었던 점
-
더보기 버튼 눌렀을 때 메모 삭제 후 커서가 남아있는 현상
-
테이블뷰에 제목, 날짜, 본문이 잘 반영되고 있었는데 어느순간 body가 반영이 되지가 않음
-
textViewDidChange
메소드 내부에서 메모의 생성날짜 업데이트가 항상 진행되는 부분 -
나의 태도
- 처음 오토레이아웃 경고창이 나왔을 때 우선 화면에 잘 보이니 나중에 고쳐보자는 마음으로 넘어갔다. 하지만 아래와 같은 문제가 나중에 발생하였다. 버그엔 사소한게 없다. 해결하고 넘어가자
- 커밋할 때 빌드되는지 확인안하고 간것. 어느순간 커밋에만 집중했던 것 같다(그 내용이 아니라). 근데 이게 쉽게고쳐지지가 않을 것 같다. 기본적인 것이 가장 중요하니 항상 유의하도록 하자
- 옵셔널을 많이 쓰는 것같다... 옵셔널을 과하게 사용하면 어디서 nil인지 알수가 없다는 단점이 있다!!
- 속이시원하다
<리팩토링 전> | <리팩토링 후> |
---|---|