✅ 코드 리뷰를 해보자.
그냥 다른 사람 좋은 코드를 보는 것만으로도 실력 많이 느는거 같아 ㅎㅅㅎ
나중에 MVVM과 Rx로 바꾸는 방법도 고민해보자.
✅ String+
//
// String+.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/08.
//
import Foundation
extension String {
// html을 string으로 변환해주는 코드
var htmlToString: String {
guard let data = self.data(using: .utf8) else { return "" }
do {
return try NSAttributedString(
data: data,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue
],
documentAttributes: nil
).string
} catch {
return ""
}
}
}
Utils에서 이렇게 사용하던데, 근데 이번에 진짜 많이 배웠다고 느끼는게, 예전에 API로 내려받았는데 html tag가 지워지지가 않고 보여지길래 이런게 있는지 모르고 text를 검사하면서 하나하나.. 내가 직접 조건에 맞게 없애는 알고리즘을 만들었었다.
근데, 이게 이런 방법이 있다고..?
하.. 정말 이렇게 또 하나 배워갈 수 있어서 감사하다 🙏
✅ Model 폴더
//
// NewRequestModel.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/07.
//
import Foundation
struct NewsRequestModel: Codable {
/// 시작 인덱스. 1 ~ 1000
let start: Int
/// 검색 결과 출력 건수, 10 ~ 1000
let display: Int
/// 검색어
let query: String
}
⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
//
// NewsResponseModel.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/07.
//
import Foundation
struct NewsResponseModel: Codable {
var items: [News] = []
}
struct News: Codable {
let title: String
let link: String
let description: String
let pubDate: String
}
✅
//
// NewsSearchManager.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/07.
//
import Alamofire
import Foundation
protocol NewsSearchManagerProtocol {
func request(
from keyword: String,
start: Int,
display: Int,
completionHandler: @escaping ([News]) -> Void
)
}
struct NewsSearchManager: NewsSearchManagerProtocol {
func request(
from keyword: String,
start: Int,
display: Int,
completionHandler: @escaping ([News]) -> Void
) {
guard let url = URL(string: "https://openapi.naver.com/v1/search/news.json") else { return }
let parameters = NewsRequestModel(start: start, display: display, query: keyword)
let headers: HTTPHeaders = [
"X-Naver-Client-id": "{Your Api Key}",
"X-Naver-Client-Secret": "{Your Api Key}"
]
AF
.request(url, method: .get, parameters: parameters, headers: headers)
.responseDecodable(of: NewsResponseModel.self) { response in
switch response.result {
case .success(let result):
completionHandler(result.items)
case .failure(let error):
print(error)
}
}
.resume()
}
}
AF를 사용하면서 바로 디코딩하는게 정말 인상적이다 메모..하고 기억..!
✅ NewsListViewController
//
// NewsListViewController.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/05.
//
import UIKit
import SnapKit
import Then
class NewsListViewController: UIViewController {
private lazy var presenter = NewsListPresenter(viewController: self)
private lazy var refreshControl = UIRefreshControl().then {
$0.addTarget(self, action: #selector(didCalledRefresh), for: .valueChanged)
}
private lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.delegate = presenter
tableView.dataSource = presenter
tableView.register(
NewsListTableViewCell.self,
forCellReuseIdentifier: NewsListTableViewCell.identifier
)
// 헤더등록 코드가 조금 다르다.
tableView.register(
NewsListTableViewHeaderView.self,
forHeaderFooterViewReuseIdentifier: NewsListTableViewHeaderView.identifier
)
tableView.refreshControl = refreshControl
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
presenter.viewDidLoad()
NewsSearchManager().request(from: "아이폰", start: 1, display: 20) { newsArray in
print(newsArray)
}
}
}
extension NewsListViewController: NewsListProtocol {
func setupNavigationBar() {
navigationItem.title = "NEWS"
navigationController?.navigationBar.prefersLargeTitles = true
}
func setupLayout() {
view.addSubview(tableView)
tableView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
func endRefreshing() {
refreshControl.endRefreshing()
}
func moveToNewsWebViewController(with news: News) {
let newWebController = NewWebViewContrller(news: news)
navigationController?.pushViewController(newWebController, animated: true)
}
func reloadTableView() {
tableView.reloadData()
}
}
private extension NewsListViewController {
@objc func didCalledRefresh() {
presenter.didCalledRefresh()
}
}
✅ 여기도 워낙 배울게 많아서..!
//
// NewsListPresenter.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/05.
//
import Foundation
import UIKit
protocol NewsListProtocol: AnyObject {
func setupNavigationBar()
func setupLayout()
func endRefreshing()
func moveToNewsWebViewController(with news: News)
func reloadTableView()
}
final class NewsListPresenter: NSObject {
private weak var viewController: NewsListProtocol?
private let newsSearchManager: NewsSearchManagerProtocol
private var newsList: [News] = []
private var currentKeyword = ""
/// 지금까지 request된, 가지고 있는 보여주고 있는 페이지가 어디인지
private var currentPage: Int = 0
/// 한 페이지에 최대 몇 개까지 보여줄 것인지
private let display: Int = 20
private let tags: [String] = ["IT", "아이폰", "개발", "개발자", "판교", "게임", "앱개발", "강남", "스타트업"]
init(
viewController: NewsListProtocol,
newsSearchManager: NewsSearchManagerProtocol = NewsSearchManager()
) {
self.viewController = viewController
self.newsSearchManager = newsSearchManager
}
func viewDidLoad() {
viewController?.setupNavigationBar()
viewController?.setupLayout()
}
func didCalledRefresh() {
requestNewsList(isNeededToReset: true)
}
}
extension NewsListPresenter: NewsListTableViewHeaderViewDelegate {
func didSelectTag(_ selectedIndex: Int) {
currentKeyword = tags[selectedIndex]
requestNewsList(isNeededToReset: true)
}
}
extension NewsListPresenter: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let news = newsList[indexPath.row]
viewController?.moveToNewsWebViewController(with: news)
}
// 안보이는 셀이 보여지려고 하면 이 메소드가 호출된다. -> 페이지 네이션을 위해 (무한 스크롤)
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let currentRow = indexPath.row // 현재 보여지고 있는 Row
// 끝에 닿기 3개 전에 이 작업 작업 실행하기 위한 guard 문
guard (currentRow % 20) == display - 3 && (currentRow / display) == (currentPage - 1) else { return } // 네이버의 currentPage가 1부터 시작하기 때문
requestNewsList(isNeededToReset: false)
}
}
extension NewsListPresenter: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
newsList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(
withIdentifier: NewsListTableViewCell.identifier,
for: indexPath
) as? NewsListTableViewCell else { return UITableViewCell() }
let news = newsList[indexPath.row]
cell.setup(news: news)
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(
withIdentifier: NewsListTableViewHeaderView.identifier
) as? NewsListTableViewHeaderView
header?.setup(tags: tags, delegate: self)
return header
}
}
private extension NewsListPresenter {
// 페이지 네이션을 통하여 구현되었음.
func requestNewsList(isNeededToReset: Bool) {
// reset을 하는 이유는 스크롤을 무한히 내렸다가 위에서 refreshControl을 할 경우 스크롤 바퀴가 다시 커지게 끔 조정하기 위함. 즉 위에서 리프레시를 실시할 경우 셀이 20개로 맞춰진다.
if isNeededToReset {
currentPage = 0
newsList = []
}
newsSearchManager.request(
from: currentKeyword,
start: (currentPage * display) + 1, // 네이버 API는 시작점이 1부터라서
display: 20) { [weak self] newValue in
self?.newsList += newValue // 페이지 네이션을 위한 코드(대입이 아니라 더하기)
self?.currentPage += 1
self?.viewController?.reloadTableView()
self?.viewController?.endRefreshing()
}
}
}
무한 스크롤을 구현하는 코드인데 독특해!
✅ NewsListTableViewCell
//
// NewsListTableViewCell.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/05.
//
import Foundation
import SnapKit
import Then
import UIKit
import TTGTags
final class NewsListTableViewCell: UITableViewCell {
static let identifier = "NewsListTableViewCell"
private lazy var titleLabel = UILabel().then {
$0.font = .systemFont(ofSize: 15.0, weight: .semibold)
}
private lazy var descriptionLabel = UILabel().then {
$0.font = .systemFont(ofSize: 14.0, weight: .medium)
}
private lazy var dateLabel = UILabel().then {
$0.font = .systemFont(ofSize: 12.0, weight: .medium)
$0.textColor = .secondaryLabel
}
func setup(news: News) {
setupLayout()
accessoryType = .disclosureIndicator
selectionStyle = .none
titleLabel.text = news.title.htmlToString
descriptionLabel.text = news.description.htmlToString
dateLabel.text = news.pubDate.htmlToString
}
}
private extension NewsListTableViewCell {
// 셀 크기를 height으로 지정해주지 않아도 자동으로 잡힘!
func setupLayout() {
[titleLabel, descriptionLabel, dateLabel]
.forEach { addSubview($0) }
let superViewInset: CGFloat = 16.0
titleLabel.snp.makeConstraints {
$0.leading.equalToSuperview().inset(superViewInset)
$0.trailing.equalToSuperview().inset(48.0)
$0.top.equalToSuperview().inset(superViewInset)
}
let verticalSpacing: CGFloat = 4.0
descriptionLabel.snp.makeConstraints{
$0.leading.equalTo(titleLabel.snp.leading)
$0.trailing.equalTo(titleLabel.snp.trailing)
$0.top.equalTo(titleLabel.snp.bottom).offset(verticalSpacing)
}
dateLabel.snp.makeConstraints {
$0.leading.equalTo(titleLabel.snp.leading)
$0.trailing.equalTo(titleLabel.snp.trailing)
$0.top.equalTo(descriptionLabel.snp.bottom).offset(verticalSpacing)
$0.bottom.equalToSuperview().inset(superViewInset)
}
}
}
✅ NewsListTableViewHeaderView
//
// NewsListTableViewHeaderView.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/05.
//
import UIKit
import SnapKit
import TTGTags
protocol NewsListTableViewHeaderViewDelegate: AnyObject {
func didSelectTag(_ selectedIndex: Int)
}
final class NewsListTableViewHeaderView: UITableViewHeaderFooterView {
static let identifier = "NewsListTableViewHeaderView"
private lazy var tagCollectionView = TTGTextTagCollectionView()
private weak var delegate: NewsListTableViewHeaderViewDelegate?
private var tags: [String] = []
func setup(
tags: [String],
delegate: NewsListTableViewHeaderViewDelegate
) {
self.tags = tags
self.delegate = delegate
contentView.backgroundColor = .systemBackground
setupTagCollectionViewLayout()
setupTagCollectionView()
}
}
extension NewsListTableViewHeaderView: TTGTextTagCollectionViewDelegate {
func textTagCollectionView(_ textTagCollectionView: TTGTextTagCollectionView!, didTap tag: TTGTextTag!, at index: UInt) {
print("textTagCollectionView \(tag)")
guard tag.selected else { return } // 태그가 셀렉 되었을 때만 불려지게끔 필터링 해주기 -> 태그가 셀렉되지 않은 상태로 조건이 변경될 때도 불려지기 때문에
delegate?.didSelectTag(Int(index))
}
func textTagCollectionView(_ textTagCollectionView: TTGTextTagCollectionView!, canTap tag: TTGTextTag!, at index: UInt) -> Bool {
print("canTap")
return true
}
}
private extension NewsListTableViewHeaderView {
func setupTagCollectionViewLayout() {
addSubview(tagCollectionView)
tagCollectionView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
func setupTagCollectionView() {
tagCollectionView.delegate = self
tagCollectionView.numberOfLines = 1
tagCollectionView.scrollDirection = .horizontal
tagCollectionView.showsVerticalScrollIndicator = false
tagCollectionView.selectionLimit = 1
let insetValue: CGFloat = 16.0
tagCollectionView.contentInset = UIEdgeInsets(
top: insetValue,
left: insetValue,
bottom: insetValue,
right: insetValue
)
let cornerRadiusValue: CGFloat = 12.0
let shadowOpacity: CGFloat = 0.0
let extraSpace = CGSize(width: 20.0, height: 12.0)
let color = UIColor.systemOrange
let style = TTGTextTagStyle()
style.backgroundColor = color
style.cornerRadius = cornerRadiusValue
style.borderWidth = 0.0
style.shadowOpacity = shadowOpacity
style.extraSpace = extraSpace
let selectedStyle = TTGTextTagStyle()
selectedStyle.backgroundColor = .white
selectedStyle.cornerRadius = cornerRadiusValue
selectedStyle.shadowOpacity = shadowOpacity
selectedStyle.extraSpace = extraSpace
selectedStyle.borderColor = color
tags.forEach { tag in
let font = UIFont.systemFont(ofSize: 14.0, weight: .semibold)
let tagContents = TTGTextTagStringContent(
text: tag,
textFont: font,
textColor: .white
)
let selectedTagContents = TTGTextTagStringContent(
text: tag,
textFont: font,
textColor: color
)
let tag = TTGTextTag(
content: tagContents,
style: style,
selectedContent: selectedTagContents,
selectedStyle: selectedStyle
)
tagCollectionView.addTag(tag)
}
}
}
이건 서드파티 라이브러리를 사용한 코드이다.
따로 포스팅을 하자!
✅ NewWebViewContrller
//
// NewWebView.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/07.
//
import Foundation
import WebKit
import UIKit
import SnapKit
final class NewWebViewContrller: UIViewController {
private let news: News
private let webView = WKWebView()
private var rightBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "link"),
style: .plain,
target: self,
action: #selector(didTapRightBarButtonItem))
init(news: News) {
self.news = news
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupNavigationbBar()
setupWebView()
}
}
private extension NewWebViewContrller {
func setupNavigationbBar() {
navigationItem.title = news.title.htmlToString
navigationItem.rightBarButtonItem = rightBarButtonItem
}
func setupWebView() {
guard let linkURL = URL(string: news.link) else {
// 링크가 없다면 자동으로 팝해주면 UX적으로 더 좋은듯
navigationController?.popViewController(animated: true)
return
}
view = webView
let urlRequest = URLRequest(url: linkURL)
webView.load(urlRequest)
}
@objc func didTapRightBarButtonItem() {
UIPasteboard.general.string = news.link // 클립 보드에 복사하는 메소드
}
}
✅ SceneDelegate
//
// SceneDelegate.swift
// KeywordNews
//
// Created by Hamlit Jason on 2022/02/05.
//
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.backgroundColor = .systemBackground
window?.tintColor = .systemOrange // 네비게이션 바 같은 컬러를 한번에 바꿀 수 있다.
window?.rootViewController = UINavigationController(rootViewController: NewsListViewController())
window?.makeKeyAndVisible()
}
}
해야할 일 - 코드 리뷰 하나하나 분석
테스크 코드도 따로 포스팅
깃허브에 올릴때 API 키 삭제하기
'Archive > 패캠(초격차)' 카테고리의 다른 글
part5 (ch1). FindCVS 코드리뷰 (0) | 2022.02.20 |
---|---|
part5 (ch6). KeywordNews XCTest 코드리뷰 (0) | 2022.02.17 |
part5 (ch6). 🪛 CI/CD란? (feat. bitrise) (0) | 2022.02.15 |
part5 (ch6). MovieReview XCTest 코드리뷰 (0) | 2022.02.01 |
part5 (ch6). BDD의 개념에 대해서 알아보기 (0) | 2022.02.01 |