10주차 과제 (1)
이번에도 과제가 주어졌다.
사실 MVC로하면 길어야 이틀짜리 과제인데, 이번엔 좀 새로운 시도를 해보고자 컴바인을 공부를 하면서 mvvm도 약간 공부를 했는데,
그래서 이걸 적용을 해서 과제를 해보면 아주 Best of Best 일것 같아서 이렇게 과제를 해보려 한다.
물론 TableView, CollectionView 도 DataSource가 아닌 Diffable로 해보려고한다. (가능하면?)
하다가 안되면 MVC로 돌리면 되는거고.. 사실 MVC는 할만큼 했고 알만큼 아니까 MVVM을 해보는게 제일 좋긴하다.
과제는 다음과 같다.
Level 1 - 화면 구성
예시 화면 가이드입니다. 참고하시고 요구사항을 벗어나지 않는 선에서 자유롭게 구성하시면 됩니다.
- 2개의 탭 과 3개의 화면을 가진 앱입니다.
UITabBarController
을 사용하여 2개의 탭을 구현합니다.
- 책 검색 화면
- 첫 번째 탭에 위치합니다.
- 책 상세 화면
- 사용자는 검색 결과의 리스트 아이템을 ‘탭’하여 책 상세 화면에 진입할 수 있습니다.
- 책 상세 화면은
모달 방식
으로 등장합니다.
- 담은 책 리스트 화면
- 두번째 탭에 위치합니다.
- 사용자는 책 상세 화면에서
담기
를 한 책 리스트를 저장한 책 리스트 화면에서 볼 수 있습니다.
Level 2 - 책 검색 화면 구현
- 화면 구성
- 사용자는 서치바를 이용해서 책을 검색합니다.
- UISearchBar, UITextField 등을 활용
- 사용자는 검색 이후 검색 결과를 리스트를 통해 볼 수 있습니다.
- 검색 결과 리스트는 컬렉션뷰(혹은 테이블뷰)로 구현합니다.
- FlowLayout 을 사용하셔도 되고,
- 컬렉션뷰을 사용하시는 경우 CompositionalLayout 을 활용하셔도 좋습니다. (Level4을 구현하신다면 시도해보셔도 좋습니다.)
- 사용자는 서치바를 이용해서 책을 검색합니다.
- 검색 기능
- 사용자는 서치바를 사용하여 책을 검색할 수 있습니다.
- 검색(입력완료)를 누르면, 검색 결과 리스트에 책 목록이 등장합니다.
- 검색에는 카카오 책 검색 REST API 를 이용합니다.
- Kakao Developers 검색 제품의 **
책 검색 기능
을 사용합니다.- https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide#search-book
- Kakao Developers 검색 제품의 **
Level 3 - 책 상세 보기 & 담기 기능 구현
1. 책 상세 화면
- 책 상세 화면에서는 검색 결과 응답 내용을 자세하게 보여줍니다.
- title
- authors
- contents
- thumbnail
- 등
담기
버튼을 탭하면- 해당 책은 담은 책 목록 화면에서 볼 수 있습니다.
- 모달은 닫힙니다.
X
버튼을 탭 하면 모달은 닫힙니다.- X 와 담기 버튼의 너비 비율은 1:3~4 정도이면 될 것 같습니다.
- (선택 구현) 책 상세 화면은 컨텐츠 양에 따라 스크롤 가능합니다.
(선택 구현) 담기 및 X 버튼은 플로팅 버튼입니다.
즉, 스크롤과 상관없이 항상 화면 위에 노출되어야합니다.
- (선택 구현) 모달이 닫힌 이후, 책 검색 화면에서
[…]책 담기 완료!
라는 알림창을 보여줍니다.- Delegate 패턴을 활용해봅니다.
2. 담은 책 목록 화면
- 담은 책 목록 화면은 두번째 탭에 위치합니다.
- 앱을 종료하고 다시 시작해도 담은 책 목록은 남아있어야합니다.
- 전체 삭제 버튼을 누르면 담았던 모든 책이 지워집니다.
- 스와이프 등의 방식을 통하여 담은 책 개별삭제가 가능합니다.
- (선택 구현) 추가 버튼을 누르면 첫번째 탭을 보여주고, 서치바를 활성화시킵니다.
- UITabBarController
- First Responder
우선은 lv3까지 해보려고 한다.
Level 1
코드로 UIDesign 시작.
StoryBoard 삭제.
이때 삭제를 스토리보드 삭제말고 2가지를 더 삭제해야한다.
1. SceneDelegate 수정.
StoryBoard로 구현하기 위해 SceneDelegate에서 기초 작업을 해줘야 한다.
1
2
3
4
5
6
7
8
9
10
11
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let vc = ViewController()
window.rootViewController = vc
self.window = window
window.makeKeyAndVisible()
}
viewDidLoad에서 backgroundColor를 blue로 해두었다.
작동 확인.
2. SnapKit 설치
이건 뭐 계속 써야하니 설치를 하고 시작하는게 좋을듯 하다.
SPM으로 설치한다.
CocoaPods은 설치하면 실행파일 달라져서 패스.
3. UIDesign
우선 사진에 있는 내용을 크게 4개의 section으로 분리를 했다.
- SearchView
- RecentView
- ResultView
- Tabbar -> tabbar로 구현 / Button X
탭바뷰는 버튼으로 할지 탭바로 할지 고민이 되었으나,
코드로 탭바를 구성을 해본적이 없으니 공부도 할겸 탭바는 코드로 결정
1. Tabbar 구현
우선 탭바를 구현 해둬야 레이아웃을 잡기 편할것 같다는 생각이 들었다.
검색을 해보니
참고사이트에 좋은 글이 있어서 이걸 기반으로 작성 해본다.
Tabbar도 SceneDelegate에서 설정을 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let vc = ViewController()
self.window = window
window.makeKeyAndVisible()
// added
let tabbarController = UITabBarController()
let firstVC = UINavigationController(rootViewController: vc)
firstVC.tabBarItem = UITabBarItem(
title: "Search",
image: UIImage(systemName: "magnifyingglass.circle"),
selectedImage: UIImage(systemName: "magnifyingglass.circle.fill"))
tabbarController.viewControllers = [firstVC]
window.rootViewController = tabbarController // modifed
}
이렇게 탭바를 추가한다.
탭바 백그라운드가 있어야 할것 같아서
tabbarController.tabBar.backgroundColor = .white
이것도 추가해둔다.
2. 틀 잡기.
Fok형님의 방법을 적용하여 큰틀에서의 UI구성을 해보았다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ViewController: UIViewController {
private let searchView = SearchView()
private let recentView = RecentView()
private let resultView = ResultView()
private lazy var vStackView : UIStackView = {
let stackView = UIStackView(arrangedSubviews: [
searchView,
recentView,
resultView,
UIView()
])
stackView.axis = .vertical
stackView.spacing = 25
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
layout()
}
private func layout() {
view.addSubview(vStackView)
vStackView.snp.makeConstraints { make in
make.top.equalTo(view.snp.top).offset(100)
make.leading.trailing.bottom.equalToSuperview()
}
searchView.snp.makeConstraints { make in
make.height.equalTo(65)
}
recentView.snp.makeConstraints { make in
make.height.equalTo(180)
}
resultView.snp.makeConstraints { make in
make.height.equalTo(356)
}
}
}
파란색쪽에 searchbar가 들어가고
갈색에 CollectionView
초록색에 검색결과의 TableView가 들어갈 예정
navigatorbar 위치를 고려해 80에서 100으로 변경.
3. SearchView 구현
우선 대충 구현한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SearchView: UIView {
private let searchBar: UISearchBar = {
let bar = UISearchBar()
bar.placeholder = "검색어를 입력하세요."
bar.autocorrectionType = .no
return bar
}()
init () {
super.init(frame: .zero)
layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func layout() {
addSubview(searchBar)
searchBar.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.bottom.equalToSuperview().offset(-10)
make.leading.equalToSuperview().offset(10)
make.trailing.equalToSuperview().offset(-10)
}
}
}
searchBar에 대한 값 처리는 나중에 Combine을 통해서 구현 예정
4. RecentView 구현
여기에 필요한건 CollectionView와 UILabel이다.
그중에서도 main은 collectionview
1
2
3
4
5
6
7
8
9
10
11
class TextLabel: UILabel {
func makeLabel (textValue: String) -> UILabel {
let label = UILabel()
let text = NSMutableAttributedString(string: textValue, attributes: [.font: UIFont.systemFont(ofSize: 24)])
label.attributedText = text
return label
}
}
비슷한게 또 ResultView에서 사용이 되어서 그냥 클래스로 만들어 주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class RecentView: UIView {
private var textLabel = TextLabel().makeLabel(textValue: "최근 본 책")
init () {
super.init(frame: .zero)
layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func layout() {
addSubview(textLabel)
textLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(5)
make.leading.equalToSuperview().offset(20)
make.width.equalTo(100)
make.height.equalTo(50)
}
}
}
이제 컬렉션뷰를 구현해보도록 한다.
1
2
3
4
5
6
7
8
private var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
var view = UICollectionView(frame: .zero, collectionViewLayout: layout)
return view
}()
우선 이렇게만 해둔상태.
그리고 label과 collectionView를 아우르는 StackView를 하나더 생성 (이게 Fok형 Style)
셀을 만들고 ImageView만 넣을 생각
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class RecentCollectionViewCell: UICollectionViewCell {
private var imageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.image = UIImage(systemName: "book")
view.tintColor = .white
view.clipsToBounds = true
return view
}()
func configure(image: UIImage) {
self.imageView.image = image
self.layout()
}
func layout() {
self.backgroundColor = .white
self.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.leading.bottom.trailing.top.equalToSuperview()
}
}
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.image = nil
}
}
그리고 cell 등록도 해준다.
1
2
3
4
5
6
7
8
9
private var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
var view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .blue
view.register(RecentCollectionViewCell.self, forCellWithReuseIdentifier: Constants.collectionViewCellIdentifier) // added
return view
}()
하지만 여기서 문제가 생기는건? 바로 delegate와 datasource를 어떻게 처리할것인가이다.
우선 그 고민은 나중에 다시해보는걸로..
일단 Stackview를 씌우고 실행했을때 error가 발생하던건 모두 해결했다.
CollectionView의 bottom이 안먹던것은 uiview를 하나더 추가하면서 해결
WTFautolayout사이트를 통해 조절을 했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private func layout() {
addSubview(vStackView)
vStackView.snp.makeConstraints { make in
make.top.bottom.leading.trailing.equalToSuperview()
}
textLabel.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.equalTo(vStackView.snp.leading).offset(20)
make.trailing.equalTo(vStackView.snp.trailing).offset(-20)
}
collectionView.snp.makeConstraints { make in
make.top.equalTo(textLabel.snp.bottom).offset(5)
make.leading.equalTo(vStackView.snp.leading).offset(20)
make.trailing.equalTo(vStackView.snp.trailing).offset(-20)
make.bottom.equalTo(vStackView.snp.bottom).offset(-15)
}
}
수정한 layout
적용 완료.
5. ResultView 구현
여기엔 UILable, TableView가 들어오면 될것같다.
Cell 구성은 다음과 같다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class ResultTableViewCell: UITableViewCell {
private let titleLabel = TextLabel().makeLabel(value: "Title")
private let priceLabel = TextLabel().makeLabel(value: "Price")
private lazy var hStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [
titleLabel,
priceLabel
])
stackView.axis = .horizontal
stackView.alignment = .center
return stackView
}()
override func awakeFromNib() {
super.awakeFromNib()
layout()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
private func layout () {
addSubview(hStackView)
hStackView.snp.makeConstraints { make in
make.leading.bottom.trailing.top.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.leading.equalTo(hStackView.snp.leading).offset(10)
make.top.equalTo(hStackView.snp.top).offset(10)
make.bottom.equalTo(hStackView.snp.bottom).offset(-10)
make.width.equalTo(150)
}
priceLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel.snp.trailing).offset(-25)
make.trailing.equalTo(hStackView.snp.trailing).offset(-10)
make.top.equalTo(hStackView.snp.top).offset(10)
make.bottom.equalTo(hStackView.snp.bottom).offset(-10)
make.width.equalTo(100)
}
}
}
아직 sample이 없어 자세한 확인은 불가.
추후에 Autolayout에 대한 값이 수정이 될듯 하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class ResultView: UIView {
private let textLabel = TextLabel().makeLabel(textValue: "검색 결과")
private let tableView: UITableView = {
let tableView = UITableView()
tableView.backgroundColor = .cyan
tableView.allowsSelection = false
tableView.register(ResultTableViewCell.self, forCellReuseIdentifier: Constants.tableViewCellIdentifier)
return tableView
}()
private lazy var vStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [
textLabel,
tableView,
])
stackView.axis = .vertical
stackView.spacing = 10
stackView.alignment = .center
return stackView
}()
init () {
super.init(frame: .zero)
layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func layout() {
addSubview(vStackView)
vStackView.snp.makeConstraints { make in
make.top.bottom.leading.trailing.equalToSuperview()
}
textLabel.snp.makeConstraints { make in
make.top.equalTo(vStackView.snp.top)
make.leading.equalTo(vStackView.snp.leading).offset(20)
make.trailing.equalTo(vStackView.snp.trailing).offset(-20)
make.bottom.equalTo(tableView.snp.top).offset(-10)
}
tableView.snp.makeConstraints { make in
make.leading.equalTo(vStackView.snp.leading).offset(20)
make.trailing.equalTo(vStackView.snp.trailing).offset(-20)
make.bottom.equalTo(vStackView.snp.bottom)
}
}
}
우선 이렇게 구현을 했다.
autolayout에 대한 error는 모두 해결 (Cell 제외)