-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
오픈 마켓 [STEP 2] 애플사이다, 허황 #96
Changes from all commits
66ebfdf
9b5bd96
7e45f05
3a6e226
ca190dd
8f8e8ad
c9aa8e7
090979a
4effbec
5178c12
7e15350
a1638f3
1c8cb1b
503d9ac
17e9165
8b0087b
ad32610
cb18939
c767a0a
e8f8127
a54302b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import UIKit | ||
|
||
final class AddProductViewController: UIViewController { | ||
private let titleLabel: UILabel = { | ||
let label = UILabel() | ||
label.text = "상품 등록" | ||
label.textAlignment = .center | ||
label.font = .preferredFont(forTextStyle: .title1) | ||
return label | ||
}() | ||
|
||
override func viewDidLoad() { | ||
super.viewDidLoad() | ||
setupViewController() | ||
setupTitleLabel() | ||
} | ||
|
||
private func setupViewController() { | ||
view.backgroundColor = .white | ||
} | ||
|
||
private func setupTitleLabel() { | ||
view.addSubview(titleLabel) | ||
titleLabel.translatesAutoresizingMaskIntoConstraints = false | ||
titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true | ||
titleLabel.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
// | ||
// OpenMarket - ViewController.swift | ||
// Created by yagom. | ||
// Copyright © yagom. All rights reserved. | ||
// | ||
|
||
import UIKit | ||
|
||
final class OpenMarketViewController: UIViewController { | ||
// MARK: - Properties | ||
private enum LayoutKind: String, CaseIterable, CustomStringConvertible { | ||
case list = "LIST" | ||
case grid = "GRID" | ||
|
||
var description: String { | ||
return self.rawValue | ||
} | ||
|
||
var cellIdentifier: String { | ||
switch self { | ||
case .list: | ||
return ListProductCell.identifier | ||
case .grid: | ||
return GridProductCell.identifier | ||
} | ||
} | ||
|
||
var cellType: ProductCellProtocol.Type { | ||
switch self { | ||
case .list: | ||
return ListProductCell.self | ||
case .grid: | ||
return GridProductCell.self | ||
} | ||
} | ||
} | ||
|
||
private var currentLayoutKind: LayoutKind = .list | ||
private var products: [Product]? | ||
|
||
private var segmentedControl: ViewTypeSegmentedControl! | ||
private var productCollectionView: UICollectionView! | ||
private var activityIndicator: UIActivityIndicatorView! | ||
|
||
override func viewDidLoad() { | ||
super.viewDidLoad() | ||
setupViewController() | ||
setupNavigationBar() | ||
setupCollectionView() | ||
setupActivityIndicator() | ||
registerCell() | ||
|
||
setupProducts() | ||
} | ||
|
||
private func setupViewController() { | ||
view.backgroundColor = .white | ||
} | ||
|
||
private func reloadDataWithActivityIndicator(at collectionView: UICollectionView?) { | ||
collectionView?.performBatchUpdates { | ||
startActivityIndicator() | ||
collectionView?.reloadData() | ||
} completion: { [weak self] _ in | ||
self?.endActivityIndicator() | ||
} | ||
} | ||
|
||
private func setupProducts() { | ||
NetworkDataTransfer().fetchData(api: ProductPageAPI(pageNumber: 1, itemsPerPage: 100), | ||
decodingType: ProductPage.self) { [weak self] data in | ||
self?.products = data.products | ||
DispatchQueue.main.async { | ||
self?.reloadDataWithActivityIndicator(at: self?.productCollectionView) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// MARK: - NavigationBar, Segmented Control | ||
extension OpenMarketViewController { | ||
private func setupNavigationBar() { | ||
let itemsOfsegmentedControl = LayoutKind.allCases.map { $0.description } | ||
segmentedControl = ViewTypeSegmentedControl(items: itemsOfsegmentedControl) | ||
segmentedControl.addTarget(self, action: #selector(toggleViewTypeSegmentedControl), for: .valueChanged) | ||
|
||
let navigationBarItem = navigationController?.navigationBar.topItem | ||
navigationBarItem?.titleView = segmentedControl | ||
navigationBarItem?.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, | ||
target: self, | ||
action: #selector(touchUpAddProductButton)) | ||
} | ||
|
||
@objc private func toggleViewTypeSegmentedControl(_ sender: UISegmentedControl) { | ||
let currentScrollRatio: CGFloat = currentScrollRatio() | ||
currentLayoutKind = LayoutKind.allCases[sender.selectedSegmentIndex] | ||
|
||
productCollectionView.performBatchUpdates(nil) { [weak self] _ in | ||
DispatchQueue.main.async { | ||
if let nextViewMaxHeight = self?.productCollectionView.contentSize.height { | ||
let offset = CGPoint(x: 0, y: nextViewMaxHeight * currentScrollRatio) | ||
self?.productCollectionView.setContentOffset(offset, animated: false) | ||
self?.productCollectionView.reloadData() | ||
} | ||
} | ||
} | ||
} | ||
|
||
private func currentScrollRatio() -> CGFloat { | ||
return productCollectionView.contentOffset.y / productCollectionView.contentSize.height | ||
} | ||
|
||
@objc private func touchUpAddProductButton() { | ||
let addProductViewController = AddProductViewController() | ||
self.present(addProductViewController, animated: true, completion: nil) | ||
} | ||
} | ||
|
||
// MARK: - ActivityIndicator | ||
extension OpenMarketViewController { | ||
private func setupActivityIndicator() { | ||
activityIndicator = UIActivityIndicatorView() | ||
view.addSubview(activityIndicator) | ||
activityIndicator.center = view.center | ||
|
||
startActivityIndicator() | ||
} | ||
|
||
private func startActivityIndicator() { | ||
DispatchQueue.main.async { [weak self] in | ||
self?.activityIndicator.isHidden = false | ||
self?.activityIndicator.startAnimating() | ||
} | ||
} | ||
|
||
private func endActivityIndicator() { | ||
DispatchQueue.main.async { [weak self] in | ||
self?.activityIndicator.stopAnimating() | ||
self?.activityIndicator.isHidden = true | ||
} | ||
} | ||
} | ||
|
||
// MARK: - CollectionView | ||
extension OpenMarketViewController { | ||
private func setupCollectionView() { | ||
productCollectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewFlowLayout()) | ||
self.view.addSubview(productCollectionView) | ||
|
||
productCollectionView.translatesAutoresizingMaskIntoConstraints = false | ||
productCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor) | ||
.isActive = true | ||
productCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) | ||
.isActive = true | ||
productCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) | ||
.isActive = true | ||
productCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) | ||
.isActive = true | ||
|
||
productCollectionView.dataSource = self | ||
productCollectionView.delegate = self | ||
} | ||
|
||
private func registerCell() { | ||
LayoutKind.allCases.forEach { | ||
productCollectionView.register($0.cellType, forCellWithReuseIdentifier: $0.cellIdentifier) | ||
} | ||
} | ||
} | ||
|
||
// MARK: - CollectionView Data Source | ||
extension OpenMarketViewController: UICollectionViewDataSource { | ||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | ||
return products?.count ?? 0 | ||
} | ||
|
||
func collectionView(_ collectionView: UICollectionView, | ||
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | ||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: currentLayoutKind.cellIdentifier, | ||
for: indexPath) as? ProductCellProtocol else { | ||
return UICollectionViewCell() | ||
} | ||
|
||
guard let product = products?[indexPath.item] else { | ||
return UICollectionViewCell() | ||
} | ||
|
||
cell.updateView(with: product) | ||
|
||
return cell | ||
} | ||
} | ||
|
||
// MARK: - CollectionView Delegate FlowLayout | ||
extension OpenMarketViewController: UICollectionViewDelegateFlowLayout { | ||
func collectionView(_ collectionView: UICollectionView, | ||
layout collectionViewLayout: UICollectionViewLayout, | ||
sizeForItemAt indexPath: IndexPath) -> CGSize { | ||
switch currentLayoutKind { | ||
case .list: | ||
let listCellSize: (width: CGFloat, height: CGFloat) = (view.frame.width, view.frame.height * 0.077) | ||
return CGSize(width: listCellSize.width, height: listCellSize.height) | ||
case .grid: | ||
let gridCellSize: (width: CGFloat, height: CGFloat) = (view.frame.width * 0.45, view.frame.height * 0.32) | ||
return CGSize(width: gridCellSize.width, height: gridCellSize.height) | ||
} | ||
} | ||
|
||
func collectionView(_ collectionView: UICollectionView, | ||
layout collectionViewLayout: UICollectionViewLayout, | ||
insetForSectionAt section: Int) -> UIEdgeInsets { | ||
let inset: Double = 10 | ||
return UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) | ||
} | ||
|
||
func collectionView(_ collectionView: UICollectionView, | ||
layout collectionViewLayout: UICollectionViewLayout, | ||
minimumLineSpacingForSectionAt section: Int) -> CGFloat { | ||
switch currentLayoutKind { | ||
case .list: | ||
let listCellLineSpacing: CGFloat = 2 | ||
return listCellLineSpacing | ||
case .grid: | ||
let gridCellLineSpacing: CGFloat = 10 | ||
return gridCellLineSpacing | ||
} | ||
} | ||
|
||
func collectionView(_ collectionView: UICollectionView, | ||
layout collectionViewLayout: UICollectionViewLayout, | ||
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { | ||
switch currentLayoutKind { | ||
case .list: | ||
let listCellIteritemSpacing: CGFloat = 0 | ||
return listCellIteritemSpacing | ||
case .grid: | ||
let gridCellIteritemSpacing: CGFloat = 10 | ||
return gridCellIteritemSpacing | ||
} | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import UIKit | ||
|
||
extension CALayer { | ||
@discardableResult | ||
func addBorder(_ edges: [UIRectEdge], color: UIColor, width: CGFloat = 1.0, radius: CGFloat = 10.0) -> CALayer { | ||
let border = CALayer() | ||
for edge in edges { | ||
switch edge { | ||
case .top: | ||
border.frame = CGRect.init(x: 0, y: 0, width: frame.width, height: width) | ||
case .bottom: | ||
border.frame = CGRect.init(x: 0, y: frame.height - width, width: frame.width, height: width) | ||
case .left: | ||
border.frame = CGRect.init(x: 0, y: 0, width: width, height: frame.height) | ||
case .right: | ||
border.frame = CGRect.init(x: frame.width - width, y: 0, width: width, height: frame.height) | ||
case .all: | ||
borderColor = color.cgColor | ||
cornerRadius = radius | ||
borderWidth = width | ||
default: | ||
break | ||
} | ||
border.backgroundColor = color.cgColor | ||
} | ||
return border | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Foundation | ||
|
||
extension Double { | ||
func formattedWithComma() -> String { | ||
let numberFormatter = NumberFormatter() | ||
numberFormatter.numberStyle = .decimal | ||
return numberFormatter.string(for: self) ?? "" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Foundation | ||
|
||
extension Int { | ||
func formattedWithComma() -> String { | ||
let numberFormatter = NumberFormatter() | ||
numberFormatter.numberStyle = .decimal | ||
return numberFormatter.string(for: self) ?? "" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import UIKit | ||
|
||
extension String { | ||
func strikeThrough() -> NSAttributedString { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extension을 사용해서 문자열에 기능을 사용하는것과 파라미터로 문자열을 받아서 기능을 입혀서 output을 뽑느것에는 어떤 차이가 있을까요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 별도의 타입을 만들어서 인스턴스 메서드로 만들게 되면 메서드를 사용할 때마다 타입의 인스턴스를 생성해줘야 합니다. 여러가지 방법을 생각해봤습니다.
1번 방법으로 처음 개선하려 했으나 2번 방법으로 진행하게 되면 Protocol이나 CustomLabel를 따로 구현하지 않아도 되기 때문에 2번 방법을 사용해서 개선했습니다. |
||
let attributeString = NSMutableAttributedString(string: self) | ||
attributeString.addAttribute(NSAttributedString.Key.strikethroughStyle, | ||
value: NSUnderlineStyle.single.rawValue, | ||
range: NSMakeRange(0, attributeString.length)) | ||
return attributeString | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import UIKit | ||
|
||
extension UIImageView { | ||
func loadImage(of key: String) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 모든 UIImageView에서 필요한 기능일까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 STEP에서 필요한 모든 |
||
let cacheKey = NSString(string: key) | ||
if let cachedImage = ImageCacheManager.shared.object(forKey: cacheKey) { | ||
self.image = cachedImage | ||
return | ||
} | ||
|
||
DispatchQueue.global().async { | ||
guard let imageURL = URL(string: key), | ||
let imageData = try? Data(contentsOf: imageURL), | ||
let loadedImage = UIImage(data: imageData) else { | ||
return | ||
} | ||
ImageCacheManager.shared.setObject(loadedImage, forKey: cacheKey) | ||
|
||
DispatchQueue.main.async { | ||
self.image = loadedImage | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
내부 속성에 접근할 때 어떤 기준으로
self
를 붙이고 계신가요There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클로저 캡쳐리스트를 사용하거나, 함수의 매개변수 명과 프로퍼티 명이 같아 혼란을 줄 수 있을 때
self 키워드
를 사용했습니다.OpenMarketViewController.swift 148번 라인에 위와 같은 경우가 아닌데 사용한
self 키워드
는 삭제했습니다.