Skip to content
Open
1 change: 0 additions & 1 deletion OpenMarket/JSONParsingTests/JSONParsingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,4 @@ final class JSONParsingTests: XCTestCase {

XCTAssertEqual(pageNumber, product?.pageNo)
}

}
16 changes: 12 additions & 4 deletions OpenMarket/OpenMarket.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
AA0CE02C2908CB2F00A41C87 /* Vendors.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0CE02B2908CB2F00A41C87 /* Vendors.swift */; };
AA0CE0342908FE0700A41C87 /* JSONParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0CE0332908FE0700A41C87 /* JSONParsingTests.swift */; };
AA3D8160291A1B1600276292 /* JSONParsingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D815F291A1B1600276292 /* JSONParsingError.swift */; };
AA408EC029246B7800FD02A2 /* ProductCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA408EBF29246B7800FD02A2 /* ProductCollectionViewCell.swift */; };
AA408EC22924BF0400FD02A2 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA408EC12924BF0400FD02A2 /* ImageLoader.swift */; };
C70FB0FB25BEF61C00C9924E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70FB0FA25BEF61C00C9924E /* AppDelegate.swift */; };
C70FB0FD25BEF61C00C9924E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70FB0FC25BEF61C00C9924E /* SceneDelegate.swift */; };
C70FB0FF25BEF61C00C9924E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70FB0FE25BEF61C00C9924E /* ViewController.swift */; };
Expand Down Expand Up @@ -61,6 +63,8 @@
AA0CE0312908FE0700A41C87 /* JSONParsingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JSONParsingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
AA0CE0332908FE0700A41C87 /* JSONParsingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONParsingTests.swift; sourceTree = "<group>"; };
AA3D815F291A1B1600276292 /* JSONParsingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONParsingError.swift; sourceTree = "<group>"; };
AA408EBF29246B7800FD02A2 /* ProductCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCollectionViewCell.swift; sourceTree = "<group>"; };
AA408EC12924BF0400FD02A2 /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = "<group>"; };
C70FB0F725BEF61C00C9924E /* OpenMarket.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenMarket.app; sourceTree = BUILT_PRODUCTS_DIR; };
C70FB0FA25BEF61C00C9924E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C70FB0FC25BEF61C00C9924E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -111,6 +115,7 @@
children = (
3258D0DA290F6E3900C25807 /* NetWork.swift */,
325B2A7A291B3EE3009354D3 /* JSONParser.swift */,
AA408EC12924BF0400FD02A2 /* ImageLoader.swift */,
);
path = NetWork;
sourceTree = "<group>";
Expand Down Expand Up @@ -152,6 +157,7 @@
C70FB0FA25BEF61C00C9924E /* AppDelegate.swift */,
C70FB0FC25BEF61C00C9924E /* SceneDelegate.swift */,
C70FB0FE25BEF61C00C9924E /* ViewController.swift */,
AA408EBF29246B7800FD02A2 /* ProductCollectionViewCell.swift */,
);
path = Controller;
sourceTree = "<group>";
Expand Down Expand Up @@ -344,6 +350,7 @@
files = (
AA0CE02A2908CB2100A41C87 /* Images.swift in Sources */,
AA0CE02C2908CB2F00A41C87 /* Vendors.swift in Sources */,
AA408EC029246B7800FD02A2 /* ProductCollectionViewCell.swift in Sources */,
AA0CE0282908CAEE00A41C87 /* ListPage.swift in Sources */,
325B7EBC29123E5000CEAF98 /* DetailPage.swift in Sources */,
C70FB0FF25BEF61C00C9924E /* ViewController.swift in Sources */,
Expand All @@ -352,6 +359,7 @@
AA3D8160291A1B1600276292 /* JSONParsingError.swift in Sources */,
325B7EBE2913630800CEAF98 /* NetWorkError.swift in Sources */,
AA0CE0252908BD8E00A41C87 /* JSONDecodable.swift in Sources */,
AA408EC22924BF0400FD02A2 /* ImageLoader.swift in Sources */,
C70FB0FB25BEF61C00C9924E /* AppDelegate.swift in Sources */,
325B2A7B291B3EE3009354D3 /* JSONParser.swift in Sources */,
3258D0DB290F6E3900C25807 /* NetWork.swift in Sources */,
Expand Down Expand Up @@ -532,7 +540,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down Expand Up @@ -587,7 +595,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
Expand All @@ -604,7 +612,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = OpenMarket/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -623,7 +631,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = OpenMarket/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
119 changes: 119 additions & 0 deletions OpenMarket/OpenMarket/Controller/ProductCollectionViewCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// ProductCollectionViewCell.swift
// OpenMarket
//
// Created by 박도원 on 2022/11/16.
//

import UIKit

class ProductCollectionViewCell: UICollectionViewCell {
static let identifier = "cell"

private let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.distribution = .equalSpacing
stackView.spacing = 2
return stackView
}()

private var productImage: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFit
return iv
}()

private let nameLabel: UILabel = {
let nameLb = UILabel()
nameLb.numberOfLines = 0
nameLb.font = UIFont.boldSystemFont(ofSize: 18)
return nameLb
}()

private let priceLabel: UILabel = {
let priceLb = UILabel()
priceLb.numberOfLines = 0
return priceLb
}()

private let bargainPriceLabel: UILabel = {
let bgPriceLb = UILabel()
bgPriceLb.numberOfLines = 0
return bgPriceLb
}()

private let stockLabel: UILabel = {
let stockLb = UILabel()
stockLb.numberOfLines = 0
return stockLb
}()

override init(frame: CGRect) {
super.init(frame: frame)
setLayout()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}

override func prepareForReuse() {
super.prepareForReuse()
self.productImage.image = nil
self.stockLabel.text = nil
self.stockLabel.attributedText = nil
}

private func setLayout() {
[productImage, nameLabel, priceLabel, bargainPriceLabel, stockLabel].forEach {
stackView.addArrangedSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}

layer.masksToBounds = true
layer.cornerRadius = 10
layer.borderWidth = 1
layer.borderColor = UIColor.gray.cgColor

contentView.addSubview(stackView)

NSLayoutConstraint.activate([
productImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
productImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
productImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
productImage.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -100),
])
}

func configCell(product: ListPage) {
ImageLoader.loadImage(from: product.thumbnail) { image in
self.productImage.image = image
}
nameLabel.text = product.name
if product.price == product.bargainPrice {
priceLabel.text = String(product.price)
} else {
priceLabel.text = String(product.price)
if priceLabel.text != nil {
let attributeString = NSMutableAttributedString(string: priceLabel.text ?? "")

attributeString.addAttribute(.strikethroughStyle,
value: NSUnderlineStyle.single.rawValue,
range: NSMakeRange(0, attributeString.length))

priceLabel.attributedText = attributeString
}
bargainPriceLabel.text = String(product.bargainPrice)
}

if product.stock != 0 {
stockLabel.text = "잔여 수량 : " + String(product.stock)
} else {
stockLabel.text = "품절"
}
}
}

77 changes: 74 additions & 3 deletions OpenMarket/OpenMarket/Controller/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,81 @@ import UIKit
import Foundation

class ViewController: UIViewController {
let jsonParser = JSONParser()
private var productData: [Product] = []
private var network = NetWork()

private enum Section {
case main
}

private var gridCollectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, ListPage>!

override func viewDidLoad() {
super.viewDidLoad()
let a = try? jsonParser.getMockData()
print(a?.pageNo)
getData()
}

private func getData() {
network.getProductData { (result: Result<Product, Error>) in
switch result {
case .success(let data):
self.productData.insert(data, at: 0)
DispatchQueue.main.sync {
self.createGridCollectionView()
self.configDataSource(listPage: data.pages)
self.gridCollectionView.reloadData()
}
case .failure(let failure):
print(failure)
}
}
}

private func createLayout() -> UICollectionViewCompositionalLayout{
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(self.view.frame.height * 0.25))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
group.interItemSpacing = .fixed(10)

let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = CGFloat(10)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)

let layout = UICollectionViewCompositionalLayout(section: section)

return layout
}

private func createGridCollectionView() {
gridCollectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())

view.addSubview(gridCollectionView)
gridCollectionView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
gridCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
gridCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
gridCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
gridCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}

private func configDataSource(listPage: [ListPage]) {
let cellRegistration = UICollectionView.CellRegistration<ProductCollectionViewCell, ListPage> { cell, indexPath, product in
cell.configCell(product: product)
}

dataSource = UICollectionViewDiffableDataSource<Section, ListPage>(collectionView: gridCollectionView, cellProvider: { (collectionView, indexPath, itemIdentifier) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
})

var snapShot = NSDiffableDataSourceSnapshot<Section, ListPage>()
snapShot.appendSections([.main])
snapShot.appendItems(listPage)
dataSource.apply(snapShot)
}

}
2 changes: 1 addition & 1 deletion OpenMarket/OpenMarket/Model/DetailPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ enum Currency: String, Decodable {
case usd = "USD"
}

struct DetailPage: Decodable {
struct DetailPage: Decodable, Hashable {
let id: Int
let vendorId: Int
let name: String
Expand Down
2 changes: 1 addition & 1 deletion OpenMarket/OpenMarket/Model/Images.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

struct Images: Decodable {
struct Images: Decodable, Hashable {
let id: Int
let url: String
let thumbnailUrl: String
Expand Down
4 changes: 3 additions & 1 deletion OpenMarket/OpenMarket/Model/ListPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

struct ListPage: Decodable {
struct ListPage: Decodable, Hashable {
let id: Int
let vendorId: Int
let name: String
Expand All @@ -20,6 +20,8 @@ struct ListPage: Decodable {
let stock: Int
let createdAt: String
let issuedAt: String



enum CodingKeys: String, CodingKey {
case id, name, thumbnail, currency, price, stock, description
Expand Down
2 changes: 1 addition & 1 deletion OpenMarket/OpenMarket/Model/Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

struct Product: Decodable {
struct Product: Decodable, Hashable {
let pageNo: Int
let itemsPerPage: Int
let totalCount: Int
Expand Down
2 changes: 1 addition & 1 deletion OpenMarket/OpenMarket/Model/Vendors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

struct Vendors: Decodable {
struct Vendors: Decodable, Hashable {
let id: Int
let name: String
}
35 changes: 35 additions & 0 deletions OpenMarket/OpenMarket/NetWork/ImageLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// ImageLoader.swift
// OpenMarket
//
// Created by 박도원 on 2022/11/16.
//

import Foundation
import UIKit

class ImageLoader {
static func loadImage(from url: String, completion: @escaping (UIImage?) -> ()) {
if url.isEmpty {
completion(nil)
return
}

guard let imageURL = URL(string: url) else {
return
}

DispatchQueue.global(qos: .background).async {
if let data = try? Data(contentsOf: imageURL),
let image = UIImage(data: data) {
DispatchQueue.main.async {
completion(image)
}
} else {
DispatchQueue.main.async {
completion(nil)
}
}
}
}
}
Loading