diff --git a/.DS_Store b/.DS_Store index fa365ed..3c7d387 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Project 04 - TodoTDD/ToDo/.DS_Store b/Project 04 - TodoTDD/ToDo/.DS_Store new file mode 100644 index 0000000..1ac4e57 Binary files /dev/null and b/Project 04 - TodoTDD/ToDo/.DS_Store differ diff --git a/Project 04 - TodoTDD/ToDo/Others/Assets.xcassets/AppIcon.appiconset/Contents.json b/Project 04 - TodoTDD/ToDo/Others/Assets.xcassets/AppIcon.appiconset/Contents.json index 1d060ed..9221b9b 100644 --- a/Project 04 - TodoTDD/ToDo/Others/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Project 04 - TodoTDD/ToDo/Others/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -2,92 +2,97 @@ "images" : [ { "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" + "scale" : "3x", + "size" : "20x20" }, { "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" + "scale" : "3x", + "size" : "29x29" }, { "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" + "scale" : "3x", + "size" : "40x40" }, { "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" + "scale" : "2x", + "size" : "60x60" }, { "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" + "scale" : "3x", + "size" : "60x60" }, { "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" + "scale" : "1x", + "size" : "20x20" }, { "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" + "scale" : "1x", + "size" : "29x29" }, { "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" + "scale" : "1x", + "size" : "40x40" }, { "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" + "scale" : "1x", + "size" : "76x76" }, { "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" + "scale" : "2x", + "size" : "76x76" }, { "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Project 04 - TodoTDD/ToDo/Storyboards/Base.lproj/Main.storyboard b/Project 04 - TodoTDD/ToDo/Storyboards/Base.lproj/Main.storyboard index d542695..6d1b004 100644 --- a/Project 04 - TodoTDD/ToDo/Storyboards/Base.lproj/Main.storyboard +++ b/Project 04 - TodoTDD/ToDo/Storyboards/Base.lproj/Main.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -23,14 +21,14 @@ - + - + + @@ -82,7 +81,6 @@ - @@ -125,6 +123,7 @@ + @@ -138,7 +137,6 @@ - @@ -159,31 +157,25 @@ - + - - - + - - + - - - - - + + - - + @@ -228,7 +221,6 @@ - diff --git a/Project4.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Project4.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..009c162 --- /dev/null +++ b/Project4.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "snapkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SnapKit/SnapKit.git", + "state" : { + "revision" : "f222cbdf325885926566172f6f5f06af95473158", + "version" : "5.6.0" + } + } + ], + "version" : 2 +} diff --git a/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/UserInterfaceState.xcuserstate b/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/UserInterfaceState.xcuserstate index e9e1ea0..9f201ad 100644 Binary files a/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/UserInterfaceState.xcuserstate and b/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..2677817 --- /dev/null +++ b/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/xcschemes/xcschememanagement.plist b/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..303f1c2 --- /dev/null +++ b/Project4.xcworkspace/xcuserdata/wootae.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,30 @@ + + + + + SchemeUserState + + SnapKitPlayground (Playground) 1.xcscheme + + isShown + + orderHint + 3 + + SnapKitPlayground (Playground) 2.xcscheme + + isShown + + orderHint + 4 + + SnapKitPlayground (Playground).xcscheme + + isShown + + orderHint + 1 + + + + diff --git a/README.md b/README.md index ed637fc..bbc3b68 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,25 @@ -## 프로젝트 번호 : 프로젝트 이름 +# 프로젝트 4 : TodoTDD -간략한 설명 +## step1 +### 날짜 : 2023.06.07 -이 앱의 레퍼런스는 [soapyigu의 Swift-30-Projects Project 04 - TodoTDD](https://github.com/soapyigu/Swift-30-Projects/tree/master/Project%2004%20-%20TodoTDD)입니다. +### 구현사항 +- Model + - 날짜 변환 로직이 포함된 Todo 데이터 모델 구조체 구현. + - 데이터 모델을 관리하는 배열을 만들고 접근하는 관리자 클래스 구현 + - GeoCoder를 통해 문자열을 좌표로 변환하는 로직 구현 -기본 기능을 모두 구현했다면, 디자인 및 추가 기능 구현은 자유롭게 해주세요. +- View + - 테이블 뷰 커스텀 셀 구현 -## 가이드 +- Controller + - todo가 나열된 테이블 뷰 화면 구현 + - 미완료, 완료 항목 섹션으로 구분 + - 편집모드 전환을 통해 항목 이동 및 삭제 + - 상세 화면, 항목 생성 화면으로 이동 + - todo 장소를 지도에 표시하는 상세보기 화면 구현 + - todo 생성 화면 구현 -영상 가이드는 [코드스쿼드 pr연습](https://www.youtube.com/watch?v=lFinZfu3QO0)을 참조해주세요. +### 구현 화면 -1. 본인 이름으로 브랜치(ex: PAKA)를 생성한 후, 자신의 레포로 fork해주세요. - -2. fork 한 레포에서 기능 또는 화면 단위로 새 브랜치(ex: pr1)를 생성 후 작업 및 커밋합니다. - -3. 커밋했던 브랜치(pr1)에서 자신의 이름 브랜치(PAKA)로 PR을 올려주세요. - -4. 코드 리뷰를 받고 모든 수정사항을 반영한 후 `squash and merge` 옵션으로 자신의 브랜치에 merge해주세요. - -5. merge했던 브랜치(pr1)에서 fork한 레포의 main 브랜치로 checkout후 해당 브랜치(pr1)를 삭제합니다. - -6. 다음 명령어들을 순차적으로 실행합니다. - -``` - git remote add upstream https://github.com/Swift-Master/Project1-GoodAsOldPhones - - git fetch upstream `본인의 브랜치명(ex:PAKA)` - - git rebase upstream `upstream/본인의브랜치명(ex:PAKA)` -``` - -7. 2번으로 돌아가 새로운 작업을 반복합니다. - -## 실제 화면 -![시뮬레이터화면](./ToDoTDD.gif) + diff --git a/ToDoListOfTDD/ToDoListOfTDD.xcodeproj/project.pbxproj b/ToDoListOfTDD/ToDoListOfTDD.xcodeproj/project.pbxproj index d58978a..f2bb8c1 100644 --- a/ToDoListOfTDD/ToDoListOfTDD.xcodeproj/project.pbxproj +++ b/ToDoListOfTDD/ToDoListOfTDD.xcodeproj/project.pbxproj @@ -7,9 +7,18 @@ objects = { /* Begin PBXBuildFile section */ + 6E39C5862A2493A500967990 /* ModelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E39C5852A2493A500967990 /* ModelManager.swift */; }; + 6E39C5882A2495FD00967990 /* TodoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E39C5872A2495FD00967990 /* TodoItem.swift */; }; + 6E39C58A2A24C6D500967990 /* TodoListDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E39C5892A24C6D500967990 /* TodoListDataProvider.swift */; }; + 6E39C58C2A24D4B900967990 /* DetailLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E39C58B2A24D4B900967990 /* DetailLocationViewController.swift */; }; + 6E39C58F2A24DABA00967990 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E39C58E2A24DABA00967990 /* Constants.swift */; }; + 6E39C5922A24E15500967990 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6E39C5912A24E15500967990 /* SnapKit */; }; + 6E39C5942A24E44800967990 /* TodoFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E39C5932A24E44800967990 /* TodoFormViewController.swift */; }; + 6E39C5992A25111400967990 /* TodoItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E39C5982A25111400967990 /* TodoItemCell.swift */; }; + 6E39C59B2A27A0FC00967990 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E39C59A2A27A0FC00967990 /* Location.swift */; }; 6E52A5502A2108FD009A81D2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E52A54F2A2108FD009A81D2 /* AppDelegate.swift */; }; 6E52A5522A2108FD009A81D2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E52A5512A2108FD009A81D2 /* SceneDelegate.swift */; }; - 6E52A5542A2108FD009A81D2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E52A5532A2108FD009A81D2 /* ViewController.swift */; }; + 6E52A5542A2108FD009A81D2 /* TodoListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E52A5532A2108FD009A81D2 /* TodoListViewController.swift */; }; 6E52A5572A2108FD009A81D2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6E52A5552A2108FD009A81D2 /* Main.storyboard */; }; 6E52A5592A2108FE009A81D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6E52A5582A2108FE009A81D2 /* Assets.xcassets */; }; 6E52A55C2A2108FE009A81D2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6E52A55A2A2108FE009A81D2 /* LaunchScreen.storyboard */; }; @@ -36,10 +45,18 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 6E39C5852A2493A500967990 /* ModelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManager.swift; sourceTree = ""; }; + 6E39C5872A2495FD00967990 /* TodoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoItem.swift; sourceTree = ""; }; + 6E39C5892A24C6D500967990 /* TodoListDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListDataProvider.swift; sourceTree = ""; }; + 6E39C58B2A24D4B900967990 /* DetailLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailLocationViewController.swift; sourceTree = ""; }; + 6E39C58E2A24DABA00967990 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 6E39C5932A24E44800967990 /* TodoFormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoFormViewController.swift; sourceTree = ""; }; + 6E39C5982A25111400967990 /* TodoItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoItemCell.swift; sourceTree = ""; }; + 6E39C59A2A27A0FC00967990 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; 6E52A54C2A2108FD009A81D2 /* ToDoListOfTDD.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ToDoListOfTDD.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6E52A54F2A2108FD009A81D2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 6E52A5512A2108FD009A81D2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 6E52A5532A2108FD009A81D2 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 6E52A5532A2108FD009A81D2 /* TodoListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListViewController.swift; sourceTree = ""; }; 6E52A5562A2108FD009A81D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 6E52A5582A2108FE009A81D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6E52A55B2A2108FE009A81D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -56,6 +73,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6E39C5922A24E15500967990 /* SnapKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +94,45 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 6E39C58D2A24DAA800967990 /* Utils */ = { + isa = PBXGroup; + children = ( + 6E39C58E2A24DABA00967990 /* Constants.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 6E39C5952A25103700967990 /* Views */ = { + isa = PBXGroup; + children = ( + 6E39C5982A25111400967990 /* TodoItemCell.swift */, + 6E52A5552A2108FD009A81D2 /* Main.storyboard */, + 6E52A55A2A2108FE009A81D2 /* LaunchScreen.storyboard */, + ); + path = Views; + sourceTree = ""; + }; + 6E39C5962A25104000967990 /* Controllers */ = { + isa = PBXGroup; + children = ( + 6E52A5532A2108FD009A81D2 /* TodoListViewController.swift */, + 6E39C58B2A24D4B900967990 /* DetailLocationViewController.swift */, + 6E39C5932A24E44800967990 /* TodoFormViewController.swift */, + 6E39C5892A24C6D500967990 /* TodoListDataProvider.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 6E39C5972A25104B00967990 /* Models */ = { + isa = PBXGroup; + children = ( + 6E39C5872A2495FD00967990 /* TodoItem.swift */, + 6E39C5852A2493A500967990 /* ModelManager.swift */, + 6E39C59A2A27A0FC00967990 /* Location.swift */, + ); + path = Models; + sourceTree = ""; + }; 6E52A5432A2108FD009A81D2 = { isa = PBXGroup; children = ( @@ -99,12 +156,13 @@ 6E52A54E2A2108FD009A81D2 /* ToDoListOfTDD */ = { isa = PBXGroup; children = ( + 6E39C5972A25104B00967990 /* Models */, + 6E39C5962A25104000967990 /* Controllers */, + 6E39C5952A25103700967990 /* Views */, + 6E39C58D2A24DAA800967990 /* Utils */, 6E52A54F2A2108FD009A81D2 /* AppDelegate.swift */, 6E52A5512A2108FD009A81D2 /* SceneDelegate.swift */, - 6E52A5532A2108FD009A81D2 /* ViewController.swift */, - 6E52A5552A2108FD009A81D2 /* Main.storyboard */, 6E52A5582A2108FE009A81D2 /* Assets.xcassets */, - 6E52A55A2A2108FE009A81D2 /* LaunchScreen.storyboard */, 6E52A55D2A2108FE009A81D2 /* Info.plist */, ); path = ToDoListOfTDD; @@ -143,6 +201,9 @@ dependencies = ( ); name = ToDoListOfTDD; + packageProductDependencies = ( + 6E39C5912A24E15500967990 /* SnapKit */, + ); productName = ToDoListOfTDD; productReference = 6E52A54C2A2108FD009A81D2 /* ToDoListOfTDD.app */; productType = "com.apple.product-type.application"; @@ -215,6 +276,9 @@ Base, ); mainGroup = 6E52A5432A2108FD009A81D2; + packageReferences = ( + 6E39C5902A24E15500967990 /* XCRemoteSwiftPackageReference "SnapKit" */, + ); productRefGroup = 6E52A54D2A2108FD009A81D2 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -258,9 +322,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6E52A5542A2108FD009A81D2 /* ViewController.swift in Sources */, + 6E39C5882A2495FD00967990 /* TodoItem.swift in Sources */, + 6E39C59B2A27A0FC00967990 /* Location.swift in Sources */, + 6E39C5942A24E44800967990 /* TodoFormViewController.swift in Sources */, + 6E52A5542A2108FD009A81D2 /* TodoListViewController.swift in Sources */, + 6E39C5862A2493A500967990 /* ModelManager.swift in Sources */, 6E52A5502A2108FD009A81D2 /* AppDelegate.swift in Sources */, + 6E39C58A2A24C6D500967990 /* TodoListDataProvider.swift in Sources */, 6E52A5522A2108FD009A81D2 /* SceneDelegate.swift in Sources */, + 6E39C58C2A24D4B900967990 /* DetailLocationViewController.swift in Sources */, + 6E39C5992A25111400967990 /* TodoItemCell.swift in Sources */, + 6E39C58F2A24DABA00967990 /* Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -596,6 +668,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 6E39C5902A24E15500967990 /* XCRemoteSwiftPackageReference "SnapKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SnapKit/SnapKit.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6E39C5912A24E15500967990 /* SnapKit */ = { + isa = XCSwiftPackageProductDependency; + package = 6E39C5902A24E15500967990 /* XCRemoteSwiftPackageReference "SnapKit" */; + productName = SnapKit; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 6E52A5442A2108FD009A81D2 /* Project object */; } diff --git a/ToDoListOfTDD/ToDoListOfTDD/.DS_Store b/ToDoListOfTDD/ToDoListOfTDD/.DS_Store new file mode 100644 index 0000000..a972cd1 Binary files /dev/null and b/ToDoListOfTDD/ToDoListOfTDD/.DS_Store differ diff --git a/ToDoListOfTDD/ToDoListOfTDD/Base.lproj/Main.storyboard b/ToDoListOfTDD/ToDoListOfTDD/Base.lproj/Main.storyboard deleted file mode 100644 index 25a7638..0000000 --- a/ToDoListOfTDD/ToDoListOfTDD/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ToDoListOfTDD/ToDoListOfTDD/Controllers/DetailLocationViewController.swift b/ToDoListOfTDD/ToDoListOfTDD/Controllers/DetailLocationViewController.swift new file mode 100644 index 0000000..afe16bc --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Controllers/DetailLocationViewController.swift @@ -0,0 +1,89 @@ + +import UIKit +import SnapKit +import MapKit + +// MARK: - todo의 장소를 지도에 표시해주는 화면 +class DetailLocationViewController: UIViewController { + + // MARK: - 전역 변수/상수 + var item : TodoItem? + var map = MKMapView() + lazy var titleLabel = label + lazy var locationLabel = label + + // MARK: - UILabel 생성 메서드 + var label : UILabel { + return { + let label = UILabel() + label.font = .systemFont(ofSize: 17) + label.textAlignment = .center + return label + }() + } + + + override func viewDidLoad() { + super.viewDidLoad() + setUI() + } + + func setUI() { + setAttributes() + setConstraints() + } + + func setAttributes() { + guard let item = item else {return} + view.backgroundColor = .white + setLabelAttributes(item) + setMapAttributes(item) + } + + func setConstraints() { + setLabelConstraints() + setMapConstraints() + } + + // MARK: - 라벨 속성 설정 + func setLabelAttributes(_ item : TodoItem) { + titleLabel.font = .boldSystemFont(ofSize: 32) + titleLabel.text = item.title + locationLabel.text = item.todoLocation?.name + } + + // MARK: - MKMapView 속성 설정 + func setMapAttributes(_ item : TodoItem) { + guard let currentCoordinate = item.todoLocation?.coordinate else {return} + let coordinateRegion = MKCoordinateRegion(center: currentCoordinate, latitudinalMeters: 1000, longitudinalMeters: 1000) + map.setRegion(coordinateRegion, animated: true) + } + + // MARK: - 레이아웃 설정 + func setLabelConstraints() { + [titleLabel,locationLabel].forEach{view.addSubview($0)} + titleLabel.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide).offset(60) + make.width.equalTo(view.safeAreaLayoutGuide) + make.centerX.equalTo(view.safeAreaLayoutGuide) + } + locationLabel.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide).offset(330) + make.leading.equalTo(view.safeAreaLayoutGuide).offset(120) + make.centerX.equalTo(view.safeAreaLayoutGuide) + } + } + + func setMapConstraints() { + view.addSubview(map) + map.snp.makeConstraints { make in + make.top.equalTo(locationLabel.snp.bottom).offset(50) + make.bottom.equalTo(view.safeAreaLayoutGuide) + make.width.equalTo(view.safeAreaLayoutGuide) + make.centerX.equalTo(view.safeAreaLayoutGuide) + } + } + + + +} diff --git a/ToDoListOfTDD/ToDoListOfTDD/Controllers/TodoFormViewController.swift b/ToDoListOfTDD/ToDoListOfTDD/Controllers/TodoFormViewController.swift new file mode 100644 index 0000000..e339ce5 --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Controllers/TodoFormViewController.swift @@ -0,0 +1,193 @@ + +import UIKit +import SnapKit + +// MARK: - form을 작성하고 새 todo를 생성하는 화면 +class TodoFormViewController: UIViewController { + + // MARK: - 전역 변수/상수 + var datePicker = UIDatePicker() + var itemManager : ModelManager? + var pickerLabel = { + let label = UILabel() + label.text = "날짜" + label.textAlignment = .center + return label + }() + lazy var titleTextField = textField + lazy var locationTextField = textField + lazy var descriptionTextField = textField + lazy var buttonStack : UIStackView = { + let stack = UIStackView(arrangedSubviews: [cancelButton,submitButton]) + stack.axis = .horizontal + stack.distribution = .fillEqually + return stack + }() + lazy var cancelButton = { + let button = UIButton() + button.titleLabel?.font = .systemFont(ofSize: 24) + button.setTitle("취소", for: .normal) + button.setTitleColor(.systemRed, for: .normal) + button.addTarget(self, action: #selector(discardForm), for: .touchUpInside) + return button + }() + lazy var submitButton = { + let button = UIButton() + button.titleLabel?.font = .systemFont(ofSize: 24) + button.setTitle("완료", for: .normal) + button.setTitleColor(.systemGray, for: .disabled) + button.setTitleColor(.systemBlue, for: .normal) + button.isEnabled = false + button.addTarget(self, action: #selector(submitForm), for: .touchUpInside) + return button + }() + + // MARK: - UITextField 생성 메서드 + var textField : UITextField { + return { + let textField = UITextField() + textField.font = .systemFont(ofSize: 24) + textField.textAlignment = .left + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.spellCheckingType = .no + textField.clearsOnBeginEditing = false + textField.layer.cornerRadius = 5 + textField.layer.borderWidth = 1 + textField.layer.borderColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1) + return textField + }() + } + + // MARK: - 네비게이션 바의 back 버튼이 보이지 않도록 처리 + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.hidesBackButton = true + } + + override func viewDidLoad() { + super.viewDidLoad() + setUI() + } + + // MARK: - 작성 form 외의 영역 터치시 키보드가 내려가도록 합니다. + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + [titleTextField,locationTextField,descriptionTextField].forEach{$0.resignFirstResponder()} + } + + func setUI() { + setAttributes() + setConstraints() + } + + func setAttributes() { + view.backgroundColor = .white + setTextfieldAttributes() + setDatePickerAttributes() + } + + func setConstraints() { + setTextfieldConstraints() + setDatePickerConstraints() + setButtonConstraints() + } + + // MARK: - 텍스트필드 영역 속성 설정 + func setTextfieldAttributes() { + titleTextField.placeholder = " 할일 명" + locationTextField.placeholder = " 장소" + descriptionTextField.placeholder = " 상세 설명" + titleTextField.becomeFirstResponder() + // 화면 로드시 바로 키보드가 올라오도록 만듭니다 + } + + // MARK: - DatePicker 속성 설정 + func setDatePickerAttributes() { + datePicker.preferredDatePickerStyle = .wheels + datePicker.datePickerMode = .date + datePicker.locale = Locale(identifier: "ko-KR") + datePicker.timeZone = .autoupdatingCurrent + datePicker.date = Date() + datePicker.addTarget(self, action: #selector(checkForm), for: .valueChanged) + //값이 변할때마다 form 검사 메서드를 호출 + } + + // MARK: - UI 요소들 레이아웃 설정 + func setTextfieldConstraints() { + [titleTextField,locationTextField,descriptionTextField].forEach{view.addSubview($0)} + titleTextField.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide).offset(50) + make.width.equalTo(200) + make.centerX.equalTo(view.safeAreaLayoutGuide) + } + locationTextField.snp.makeConstraints { make in + make.top.equalTo(titleTextField.snp.top).offset(50) + make.width.equalTo(200) + make.centerX.equalTo(view.safeAreaLayoutGuide) + } + descriptionTextField.snp.makeConstraints { make in + make.top.equalTo(locationTextField.snp.top).offset(50) + make.width.equalTo(200) + make.centerX.equalTo(view.safeAreaLayoutGuide) + } + } + + func setDatePickerConstraints() { + view.addSubview(datePicker) + view.addSubview(pickerLabel) + pickerLabel.snp.makeConstraints { make in + make.top.equalTo(descriptionTextField.snp.top).offset(50) + make.width.equalTo(200) + make.centerX.equalTo(view.safeAreaLayoutGuide) + } + datePicker.snp.makeConstraints { make in + make.top.equalTo(pickerLabel.snp.bottom).offset(5) + make.width.equalTo(view.safeAreaLayoutGuide) + make.centerX.equalTo(view.safeAreaLayoutGuide) + } + } + + func setButtonConstraints() { + view.addSubview(buttonStack) + buttonStack.snp.makeConstraints { make in + make.top.equalTo(datePicker.snp.bottom).offset(50) + make.width.equalTo(view.safeAreaLayoutGuide) + make.centerX.equalTo(view.safeAreaLayoutGuide) + } + } + + // MARK: - form 정보로 모델 객체 생성 및 테이블 뷰 참조 배열에 추가해주는 메서드 + func createItem(title : String, description : String, date : Date, location : String) { + let newItem = TodoItem(title: title, description: description, rawDate: date, todoLocation: Location(name: location)) + itemManager?.addItem(newItem) + } + + // MARK: - cancel 버튼 선택 시 이전 화면으로 돌아갑니다 + @objc func discardForm() { + navigationController?.popViewController(animated: false) + } + + // MARK: - form 정보로 모델 객체 생성 및 리스트에 추가 후 이전 화면으로 돌아갑니다 + @objc func submitForm() { + guard let title = titleTextField.text,let location = locationTextField.text,let detail = descriptionTextField.text else {return} + + createItem(title: title, description: detail, date: datePicker.date, location: location) + navigationController?.popViewController(animated: false) + } + + // MARK: - form을 모두 입력했다면 submit 버튼을 활성화시킵니다 + @objc func checkForm() { + guard let title = titleTextField.text,let location = locationTextField.text,let detail = descriptionTextField.text else {return} + if title.count > 0 && location.count > 0 && detail.count > 0 { + submitButton.isEnabled = true + } + } +} + +// MARK: - form 입력 후 엔터(return) 입력 시 키보드를 내립니다 +extension TodoFormViewController : UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } +} diff --git a/ToDoListOfTDD/ToDoListOfTDD/Controllers/TodoListDataProvider.swift b/ToDoListOfTDD/ToDoListOfTDD/Controllers/TodoListDataProvider.swift new file mode 100644 index 0000000..f19443a --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Controllers/TodoListDataProvider.swift @@ -0,0 +1,94 @@ + +import UIKit + +// MARK: - 테이블 뷰 프로토콜 메서드가 구현된 클래스 +class TodoListDataProvider : NSObject { + var itemManager : ModelManager? +} + +extension TodoListDataProvider : UITableViewDataSource,UITableViewDelegate { + + func numberOfSections(in tableView: UITableView) -> Int { + return 2 + } + + // MARK: - 현재 섹션의 row 수를 계산하여 반환합니다 + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let itemManager = itemManager else {return 0} + var numberOfRows = 0 + + switch TodoType(rawValue : section) { + case .todo : numberOfRows = itemManager.todoQuantity + case .done : numberOfRows = itemManager.doneQuantity + default : fatalError() + } + return numberOfRows + } + + + + // MARK: - 각 셀에 todo 제목, 장소, 날짜를 할당합니다. + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: TodoItemCell.cellIdentifier) as! TodoItemCell + guard let itemManager = itemManager else {return cell} + var currentItem : TodoItem + + switch TodoType(rawValue: indexPath.section) { + case .todo : currentItem = itemManager.item(at: indexPath.row) + case .done : currentItem = itemManager.doneItem(at: indexPath.row) + default : fatalError() + } + cell.titleLabel.text = currentItem.title + cell.locationLabel.text = currentItem.todoLocation?.name + cell.dateLabel.text = currentItem.todoDate + return cell + } + + // MARK: - 셀이 선택되었을 때 row 정보를 담아 알림을 발송합니다 + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if TodoType(rawValue: indexPath.section) == .done {return} + + NotificationCenter.default.post(name: Notification.ItemSelectedNotification, object: self,userInfo: ["index":indexPath.row]) + + } + + // MARK: - 테이블 뷰 편집모드에서 셀 삭제기능 + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + guard let manager = itemManager else {return} + + // MARK: - deleteRows는 참조 배열 데이터는 삭제하지 않아 배열 데이터 먼저 지우기 + if editingStyle == .delete { + manager.clearItem(at: indexPath) + tableView.deleteRows(at: [indexPath], with: .fade) + } + tableView.reloadData() + } + + // MARK: - 섹션 별 구분을 위한 제목을 지정합니다 + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + guard let manager = itemManager else {return nil} + var sectionTitle : String? + switch TodoType(rawValue: section) { + case .done : sectionTitle = "완료한 일(\(manager.doneQuantity))" + case .todo : sectionTitle = "해야 할 일(\(manager.todoQuantity))" + default : fatalError() + } + return sectionTitle + } + + // MARK: - 테이블 뷰 편집모드에서 셀을 움직일 수 있게 해줍니다 + func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { + return true + } + + // MARK: - 섹션 간 이동 시 실제 데이터를 옮겨줍니다 + func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { + guard let manager = itemManager else {return} + if sourceIndexPath.section < destinationIndexPath.section { + manager.checkItem(at: sourceIndexPath.row) + } else { + manager.uncheckItem(at: destinationIndexPath.row) + } + tableView.reloadData() + } +} diff --git a/ToDoListOfTDD/ToDoListOfTDD/Controllers/TodoListViewController.swift b/ToDoListOfTDD/ToDoListOfTDD/Controllers/TodoListViewController.swift new file mode 100644 index 0000000..b0a8ed0 --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Controllers/TodoListViewController.swift @@ -0,0 +1,86 @@ +import UIKit +import SnapKit + +// MARK: - todo를 테이블 뷰로 보여주는 화면(root view) +class TodoListViewController: UIViewController { + + // MARK: - 전역 변수/상수 + var todoTable = UITableView() + var itemManager = ModelManager() + var dataProvider = TodoListDataProvider() + lazy var doneButton = UIBarButtonItem(title:"Done",style: .done, target: self, action: #selector(editTable)) + lazy var editButton = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTable)) + + // MARK: - form 제출 후 돌아올 때 테이블이 갱신되도록 합니다 + override func viewWillAppear(_ animated: Bool) { + todoTable.reloadData() + } + + override func viewDidLoad() { + super.viewDidLoad() + setNaviBarAttributes() + setTableAttributes() + setTableConstraints() + } + + // MARK: - 셀 등록, 셀 선택 알림 수신, 모델 관리자 객체 등록 + func setTableAttributes() { + dataProvider.itemManager = itemManager + todoTable.dataSource = dataProvider + todoTable.delegate = dataProvider + todoTable.backgroundColor = .white + todoTable.register(TodoItemCell.self, forCellReuseIdentifier: TodoItemCell.cellIdentifier) + NotificationCenter.default.addObserver(self, selector: #selector(showDetailView), name: Notification.ItemSelectedNotification, object: nil) + } + + // MARK: - 좌우 네비게이션 아이템 설정 + func setNaviBarAttributes() { + let navigationBarAppearance = UINavigationBarAppearance() + + navigationBarAppearance.backgroundColor = .white + doneButton.tintColor = .systemRed + navigationItem.leftBarButtonItem = editButton + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(showTodoForm)) + navigationItem.scrollEdgeAppearance = navigationBarAppearance + navigationItem.standardAppearance = navigationBarAppearance + navigationItem.compactAppearance = navigationBarAppearance + navigationController?.setNeedsStatusBarAppearanceUpdate() + } + + func setTableConstraints() { + view.addSubview(todoTable) + todoTable.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + // MARK: - 알림을 통해 수신된 인덱스로 선택된 todo를 mapView 화면으로 전달 및 전환 + @objc func showDetailView(_ sender : Notification) { + guard let currentIndex = sender.userInfo?["index"] as? Int else {return} + let currentItem = itemManager.item(at: currentIndex) + let nextVC = DetailLocationViewController() + nextVC.item = currentItem + + navigationController?.pushViewController(nextVC, animated: false) + + } + + // MARK: - '+' 네비게이션 아이템 선택 시 form 작성 화면으로 전환 + @objc func showTodoForm() { + let nextVC = TodoFormViewController() + nextVC.itemManager = itemManager + navigationController?.pushViewController(nextVC, animated: false) + } + + // MARK: - 'edit' 네비게이션 아이템 선택 시 테이블 뷰 편집모드 토글 + @objc func editTable(_ sender : UIBarButtonItem) { + let editState = !todoTable.isEditing + todoTable.setEditing(editState, animated: true) + if sender == editButton { + navigationItem.leftBarButtonItem = doneButton + }else { + navigationItem.leftBarButtonItem = editButton + } + } + +} diff --git a/ToDoListOfTDD/ToDoListOfTDD/Models/Location.swift b/ToDoListOfTDD/ToDoListOfTDD/Models/Location.swift new file mode 100644 index 0000000..bd2f82d --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Models/Location.swift @@ -0,0 +1,44 @@ + +import Foundation +import CoreLocation + +// MARK: - 위치 정보 객체 +class Location { + + var name : String? + var coordinate : CLLocationCoordinate2D? + + // MARK: - 생성 시 이름으로 좌표를 얻어옵니다 + init(name: String?) { + self.name = name + forwardGeocoding(address: name) + } + + // MARK: - Geocoder를 통해 문자열을 좌표로 변환합니다 + func forwardGeocoding(address: String?){ + let geocoder = CLGeocoder() + guard let address = address else {return} + + geocoder.geocodeAddressString(address, completionHandler: { + (placemarks, error) in + if error != nil { + print("Failed to retrieve location") + return + } + + var location: CLLocation? + + if let placemarks = placemarks, placemarks.count > 0 { + location = placemarks.first?.location + } + + if let location = location { + self.coordinate = location.coordinate + } + else + { + print("No Matching Location Found") + } + }) + } +} diff --git a/ToDoListOfTDD/ToDoListOfTDD/Models/ModelManager.swift b/ToDoListOfTDD/ToDoListOfTDD/Models/ModelManager.swift new file mode 100644 index 0000000..4866f12 --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Models/ModelManager.swift @@ -0,0 +1,49 @@ + +import Foundation + +// MARK: - 모델 객체를 관리하는 공유 클래스 +class ModelManager { + + private var todoGroup = [TodoItem]() // 해야할 todo 배열 + private var doneGroup = [TodoItem]() // 완료된 todo 배열 + + var todoQuantity : Int { // 해야 할 todo 수 반환 + return todoGroup.count + } + var doneQuantity : Int { // 완료된 todo 수 반환 + return doneGroup.count + } + + func addItem(_ item : TodoItem) { + todoGroup.append(item) + } + + func item(at index : Int) -> TodoItem { + return todoGroup[index] + } + + func doneItem(at index : Int) -> TodoItem { + return doneGroup[index] + } + + // MARK: - 섹션 이동 시 데이터 이동 + func checkItem(at index : Int) { + let checkedTodo = todoGroup.remove(at: index) + doneGroup.append(checkedTodo) + } + + func uncheckItem(at index : Int) { + let uncheckedTodo = doneGroup.remove(at: index) + todoGroup.append(uncheckedTodo) + } + + // MARK: - 셀 삭제시 해당 데이터 삭제 + func clearItem(at indexPath : IndexPath) { + let section = indexPath.section, row = indexPath.row + switch section { + case 0 : todoGroup.remove(at: row) + case 1 : doneGroup.remove(at: row) + default : print("fail to delete cell") + } + } +} diff --git a/ToDoListOfTDD/ToDoListOfTDD/Models/TodoItem.swift b/ToDoListOfTDD/ToDoListOfTDD/Models/TodoItem.swift new file mode 100644 index 0000000..ba30424 --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Models/TodoItem.swift @@ -0,0 +1,26 @@ + +import Foundation +import CoreLocation + +// MARK: - todo 데이터 모델 +struct TodoItem { + var title : String? + var description : String? + var todoDate : String? + var todoLocation : Location? + + init(title: String, description: String, rawDate: Date, todoLocation: Location?) { + self.title = title + self.description = description + self.todoDate = dateToString(rawDate) + self.todoLocation = todoLocation + } + + // MARK: - 입력된 날짜를 원하는 문자열 형태로 변환합니다 + func dateToString(_ date : Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MM/dd/yyyy" + return dateFormatter.string(from: date) + } + +} diff --git a/ToDoListOfTDD/ToDoListOfTDD/SceneDelegate.swift b/ToDoListOfTDD/ToDoListOfTDD/SceneDelegate.swift index 347fbaa..0cd20c2 100644 --- a/ToDoListOfTDD/ToDoListOfTDD/SceneDelegate.swift +++ b/ToDoListOfTDD/ToDoListOfTDD/SceneDelegate.swift @@ -11,12 +11,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - + + // MARK: - 첫 화면을 TodoListViewController로 설정 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windowScene) + + let naviVC = UINavigationController(rootViewController: TodoListViewController()) + + window?.rootViewController = naviVC + window?.makeKeyAndVisible() } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/ToDoListOfTDD/ToDoListOfTDD/Utils/Constants.swift b/ToDoListOfTDD/ToDoListOfTDD/Utils/Constants.swift new file mode 100644 index 0000000..19e3674 --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Utils/Constants.swift @@ -0,0 +1,14 @@ + +import Foundation + +// MARK: - 테이블 셀을 선택했을 때 발송되는 알람 +extension Notification { + static let ItemSelectedNotification = Notification.Name("ItemSelectedNotification") +} + + +// MARK: - 섹션 구분을 위한 열거형 선언 +enum TodoType : Int { + case todo + case done +} diff --git a/ToDoListOfTDD/ToDoListOfTDD/ViewController.swift b/ToDoListOfTDD/ToDoListOfTDD/ViewController.swift deleted file mode 100644 index 11194e8..0000000 --- a/ToDoListOfTDD/ToDoListOfTDD/ViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ViewController.swift -// ToDoListOfTDD -// -// Created by 최우태 on 2023/05/27. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} - diff --git a/ToDoListOfTDD/ToDoListOfTDD/Base.lproj/LaunchScreen.storyboard b/ToDoListOfTDD/ToDoListOfTDD/Views/Base.lproj/LaunchScreen.storyboard similarity index 52% rename from ToDoListOfTDD/ToDoListOfTDD/Base.lproj/LaunchScreen.storyboard rename to ToDoListOfTDD/ToDoListOfTDD/Views/Base.lproj/LaunchScreen.storyboard index 865e932..f09d945 100644 --- a/ToDoListOfTDD/ToDoListOfTDD/Base.lproj/LaunchScreen.storyboard +++ b/ToDoListOfTDD/ToDoListOfTDD/Views/Base.lproj/LaunchScreen.storyboard @@ -1,20 +1,22 @@ - - + + + - + + - + - + - + - + @@ -22,4 +24,9 @@ + + + + + diff --git a/ToDoListOfTDD/ToDoListOfTDD/Views/Base.lproj/Main.storyboard b/ToDoListOfTDD/ToDoListOfTDD/Views/Base.lproj/Main.storyboard new file mode 100644 index 0000000..52a2956 --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Views/Base.lproj/Main.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ToDoListOfTDD/ToDoListOfTDD/Views/TodoItemCell.swift b/ToDoListOfTDD/ToDoListOfTDD/Views/TodoItemCell.swift new file mode 100644 index 0000000..db7ce6c --- /dev/null +++ b/ToDoListOfTDD/ToDoListOfTDD/Views/TodoItemCell.swift @@ -0,0 +1,61 @@ + +import UIKit +import SnapKit +class TodoItemCell: UITableViewCell { + + // MARK: - 전역 상수, 변수 + static let cellIdentifier = "todoItem" + lazy var titleLabel = label + lazy var locationLabel = label + lazy var dateLabel = label + + // MARK: - UILabel 생성 계산속성(메서드) + var label : UILabel { + return { + let label = UILabel() + label.font = .systemFont(ofSize: 17) + label.textAlignment = .left + label.numberOfLines = 2 + return label + }() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UI설정 + func setUI() { + self.backgroundColor = .white + setConstraints() + } + + // MARK: - 오토 레이아웃을 위한 셀 구성요소 간의 레이아웃 설정 + func setConstraints() { + [titleLabel,dateLabel,locationLabel].forEach{contentView.addSubview($0)} + titleLabel.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(20) + make.width.equalTo(80) + make.centerY.equalToSuperview() + make.top.equalToSuperview().offset(30) + } + locationLabel.snp.makeConstraints { make in + make.leading.equalTo(titleLabel.snp.trailing).offset(20) + make.centerX.equalToSuperview() + make.centerY.equalToSuperview() + make.top.equalToSuperview().offset(30) + } + dateLabel.snp.makeConstraints { make in + make.leading.equalTo(locationLabel.snp.trailing).offset(20) + make.centerY.equalToSuperview() + make.top.equalToSuperview().offset(30) + } + } + +} + diff --git a/project4.gif b/project4.gif new file mode 100644 index 0000000..8397bac Binary files /dev/null and b/project4.gif differ