포스트

10주차 과제 (1)

이번에도 과제가 주어졌다.

사실 MVC로하면 길어야 이틀짜리 과제인데, 이번엔 좀 새로운 시도를 해보고자 컴바인을 공부를 하면서 mvvm도 약간 공부를 했는데,

그래서 이걸 적용을 해서 과제를 해보면 아주 Best of Best 일것 같아서 이렇게 과제를 해보려 한다.

물론 TableView, CollectionView 도 DataSource가 아닌 Diffable로 해보려고한다. (가능하면?)

하다가 안되면 MVC로 돌리면 되는거고.. 사실 MVC는 할만큼 했고 알만큼 아니까 MVVM을 해보는게 제일 좋긴하다.

과제는 다음과 같다.

Level 1 - 화면 구성

예시 화면 가이드입니다. 참고하시고 요구사항을 벗어나지 않는 선에서 자유롭게 구성하시면 됩니다.

예시 화면 가이드입니다. 참고하시고 요구사항을 벗어나지 않는 선에서 자유롭게 구성하시면 됩니다.

  1. 2개의 탭 과 3개의 화면을 가진 앱입니다.
    • UITabBarController 을 사용하여 2개의 탭을 구현합니다.
  2. 책 검색 화면
    • 첫 번째 탭에 위치합니다.
  3. 책 상세 화면
    • 사용자는 검색 결과의 리스트 아이템을 ‘탭’하여 책 상세 화면에 진입할 수 있습니다.
    • 책 상세 화면은 모달 방식으로 등장합니다.
  4. 담은 책 리스트 화면
    • 두번째 탭에 위치합니다.
    • 사용자는 책 상세 화면에서 담기 를 한 책 리스트를 저장한 책 리스트 화면에서 볼 수 있습니다.

Level 2 - 책 검색 화면 구현

Untitled

  1. 화면 구성
    • 사용자는 서치바를 이용해서 책을 검색합니다.
      • UISearchBar, UITextField 등을 활용
    • 사용자는 검색 이후 검색 결과를 리스트를 통해 볼 수 있습니다.
    • 검색 결과 리스트는 컬렉션뷰(혹은 테이블뷰)로 구현합니다.
      • FlowLayout 을 사용하셔도 되고,
      • 컬렉션뷰을 사용하시는 경우 CompositionalLayout 을 활용하셔도 좋습니다. (Level4을 구현하신다면 시도해보셔도 좋습니다.)
  2. 검색 기능
    • 사용자는 서치바를 사용하여 책을 검색할 수 있습니다.
    • 검색(입력완료)를 누르면, 검색 결과 리스트에 책 목록이 등장합니다.
    • 검색에는 카카오 책 검색 REST API 를 이용합니다.
      • Kakao Developers 검색 제품** 책 검색 기능을 사용합니다.
        • https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide#search-book

Level 3 - 책 상세 보기 & 담기 기능 구현

1. 책 상세 화면

Untitled

  • 책 상세 화면에서는 검색 결과 응답 내용을 자세하게 보여줍니다.
    • title
    • authors
    • contents
    • thumbnail
  • 담기 버튼을 탭하면
    • 해당 책은 담은 책 목록 화면에서 볼 수 있습니다.
    • 모달은 닫힙니다.
  • X 버튼을 탭 하면 모달은 닫힙니다.
    • X 와 담기 버튼의 너비 비율은 1:3~4 정도이면 될 것 같습니다.
  • (선택 구현) 책 상세 화면은 컨텐츠 양에 따라 스크롤 가능합니다.
  • (선택 구현) 담기 및 X 버튼은 플로팅 버튼입니다.

    즉, 스크롤과 상관없이 항상 화면 위에 노출되어야합니다.

  • (선택 구현) 모달이 닫힌 이후, 책 검색 화면에서 […]책 담기 완료! 라는 알림창을 보여줍니다.
    • Delegate 패턴을 활용해봅니다.

2. 담은 책 목록 화면

Untitled

  • 담은 책 목록 화면은 두번째 탭에 위치합니다.
  • 앱을 종료하고 다시 시작해도 담은 책 목록은 남아있어야합니다.
  • 전체 삭제 버튼을 누르면 담았던 모든 책이 지워집니다.
  • 스와이프 등의 방식을 통하여 담은 책 개별삭제가 가능합니다.
  • (선택 구현) 추가 버튼을 누르면 첫번째 탭을 보여주고, 서치바를 활성화시킵니다.
    • UITabBarController
    • First Responder

우선은 lv3까지 해보려고 한다.


Level 1

코드로 UIDesign 시작.

StoryBoard 삭제.

이때 삭제를 스토리보드 삭제말고 2가지를 더 삭제해야한다.

이건 info.plist 에서 삭제를 한 모습. CleanShot 2024-05-04 at 18 00 40@2x

이건 Build Settings에서 삭제를 한 모습. CleanShot 2024-05-04 at 18 01 03@2x

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로 해두었다.

simulator_screenshot_87285092-5E08-4FB1-9287-1467A9C77C61

작동 확인.

2. SnapKit 설치

이건 뭐 계속 써야하니 설치를 하고 시작하는게 좋을듯 하다.

SPM으로 설치한다.

CocoaPods은 설치하면 실행파일 달라져서 패스.

3. UIDesign

CleanShot 2024-05-04 at 17 27 43@2x

우선 사진에 있는 내용을 크게 4개의 section으로 분리를 했다.

  1. SearchView
  2. RecentView
  3. ResultView
  4. 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
        
    }

이렇게 탭바를 추가한다.

simulator_screenshot_803104EA-6349-4D9E-BE67-531D41A0BCCF

탭바 백그라운드가 있어야 할것 같아서

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)
        }
    }
    
}

simulator_screenshot_C245DCD1-0913-439D-AB3E-24279C4A27DB

파란색쪽에 searchbar가 들어가고

갈색에 CollectionView

초록색에 검색결과의 TableView가 들어갈 예정

CleanShot 2024-05-04 at 19 36 52@2x

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)
        }
    }
}

simulator_screenshot_0724AC90-3275-49E5-A92B-A7CBF4F70880

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)
        }
    }
    
}

simulator_screenshot_9514BDEE-28DC-49F3-926F-2044F4632A9A

이제 컬렉션뷰를 구현해보도록 한다.

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

Simulator Screenshot - iPhone 15 Pro - 2024-05-05 at 02 47 56

적용 완료.

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 제외)

simulator_screenshot_323CEBA8-B2CA-4BE1-8556-7A80B3F512C2

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.