์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
---|---|---|---|---|---|---|
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 |
- Flutter
- Swfit
- Xcode
- designpattern
- node.js
- MVVM
- tableView
- UIKit
- Kuring
- RxSwift
- raywenderlich
- XCTest
- reactorkit
- BOJ
- swift
- rxcocoa
- TCA
- combine
- ํจ์คํธ์บ ํผ์ค
- BFS
- SwiftUI
- arkit
- realm
- ํ๋ก๊ทธ๋๋จธ์ค
- CollectionView
- ๋ฐฑ์ค
- SnapKit
- ios
- Lv2
- visionOS
- Today
- Total
lgvv98
part5 (ch6). KeywordNews ์ฝ๋๋ฆฌ๋ทฐ ๋ณธ๋ฌธ
part5 (ch6). KeywordNews ์ฝ๋๋ฆฌ๋ทฐ
๐ฅ ์บ๋ฟ๋งจ 2022. 2. 15. 03:58โ ์ฝ๋ ๋ฆฌ๋ทฐ๋ฅผ ํด๋ณด์.
๊ทธ๋ฅ ๋ค๋ฅธ ์ฌ๋ ์ข์ ์ฝ๋๋ฅผ ๋ณด๋ ๊ฒ๋ง์ผ๋ก๋ ์ค๋ ฅ ๋ง์ด ๋๋๊ฑฐ ๊ฐ์ ใ ใ ใ
๋์ค์ 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 ํค ์ญ์ ํ๊ธฐ
'โ ๏ธ deprecated โ ๏ธ > ํจ์บ (์ด๊ฒฉ์ฐจ)' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
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 |