AppStore 화면 만들어보기
이러한 화면은 너무나 익숙할 것이다.
근데 이런화면 어떻게 만들지..?
다양한 방법이 떠오를 수 있다.
이번에는 ScrollView와 StackView 그리고 CollectionView의 조합으로 위와 같은 화면을 구성해 보고자 한다.
일단 필요한 라이브러리가 있다. 굳이 사용하지 않아도 되지만 편하게 UI구성을 하고 싶다면 SanpKit을 넣어주자.
방법은 아래 링크 참고
2021.10.29 - [앱 개발 도움 링크/Libraries(Github...)] - [iOS] SnapKit 사용법
ㅇㅋ 시작
화면이 어떻게 그려질지 대강 한 번 떠올려 보자.
그림이 좀 허접하지만 이런 구조로 만들면 좋을것 같다.
ScrollView 안에 ContentView를 넣고, 그 안에는 StackView,
StackView 안은 두개의 섹션을 표시할 SectionView 를 넣고
각 섹션뷰는 좌우 스크롤이 가능한 CollectionView를 넣는다.
ㅇ_ㅋ 이 정도면 된듯!
1. ViewController에서 scrollView와 contentView그리고 stackView를 갖도록한다.
일단 뷰들은 깔고 가는것이다.
StackView 크기에 따라 늘어날 contentView는 높이를 고정시키지 않도록 하자.
class ViewController: UIViewController {
private let scrollView = UIScrollView()
private let contentView = UIView()
private var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 0.0
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
}
func setupLayout() {
view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
make.bottom.leading.trailing.equalToSuperview()
}
scrollView.addSubview(contentView)
contentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalToSuperview()
}
contentView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
2. 첫 번째 섹션을 만들기
위 섹션을 만들어보려한다.
이 섹션의 셀은 아래 처럼 구성한다. 셀부터 그려보자면
import UIKit
import SnapKit
final class FeatureSectionCollectionViewCell: UICollectionViewCell {
private lazy var typeLabel: UILabel = {
let label = UILabel()
label.textColor = .systemBlue
label.font = .systemFont(ofSize: 12.0, weight: .semibold)
return label
}()
private lazy var appNameLabel: UILabel = {
let label = UILabel()
label.textColor = .label
label.font = .systemFont(ofSize: 20.0, weight: .bold)
return label
}()
private lazy var descriptLabel: UILabel = {
let label = UILabel()
label.textColor = .secondaryLabel
label.font = .systemFont(ofSize: 16.0, weight: .semibold)
return label
}()
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.layer.cornerRadius = 7.0
imageView.layer.borderWidth = 0.5
imageView.layer.borderColor = UIColor.separator.cgColor
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
func setup() {
//setupLayout
typeLabel.text = "type"
appNameLabel.text = "App name"
descriptLabel.text = "description"
imageView.backgroundColor = .lightGray
}
}
private extension FeatureSectionCollectionViewCell {
func setupLayout() {
[typeLabel,
appNameLabel,
descriptLabel,
imageView
].forEach { view in
addSubview(view)
}
typeLabel.snp.makeConstraints { make in
make.leading.trailing.top.equalToSuperview()
}
appNameLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.equalTo(typeLabel.snp.bottom)
}
descriptLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.equalTo(appNameLabel.snp.bottom).offset(4.0)
}
imageView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.equalTo(descriptLabel.snp.bottom).offset(4.0)
make.bottom.equalToSuperview().inset(8.0)
}
}
}
이제 위 셀이 그려질 CollectionView가 필요하다.
StackView에 들어갈 SectionView를 만들고 CollectionView를 갖도록 하게 하자.
final class FeatureSectionView: UIView {
fileprivate let cellIdentifier: String = "FeatureSectionCollectionViewCell"
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 32.0
layout.minimumInteritemSpacing = 0.0
layout.sectionInset = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0, right: 16.0)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.isPagingEnabled = true
collectionView.backgroundColor = .systemBackground
collectionView.showsHorizontalScrollIndicator = false
collectionView.register(FeatureSectionCollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)
return collectionView
}()
}
scrollDirection은 좌우로 움직이게 할테니, .horizontal로 해주자.
셀 크기와 Inset, 셀간격등을 조정한다.
섹션 inset을 조정하면 가운데 UI가 정렬이 된 것처럼 보이게 할 수 있다. 좌우 16.0씩 주자.
paging효과를 주면 더 깔끔한 느낌을 줄 수 있다.
좌우 스크롤인디케이터는 제거해준다. showHorizontalScrollIndicator = false
그리고 반드시 표시할 CustomCell을 등록해주자 registerCell
셀은 3개만 표시해보자.
extension FeatureSectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? FeatureSectionCollectionViewCell else { fatalError("wt...") }
cell.setup()
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
}
extension FeatureSectionView: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
CGSize(width: collectionView.frame.width - 32.0, height: frame.width)
}
}
3. StackView로 돌아가 방금 만든 FeatureSectionView를 붙인다.
private var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 0.0
let featureSectionView = FeatureSectionView(frame: .zero)
stackView.addArrangedSubview(featureSectionView)
return stackView
}()
4. 두 번째 섹션 만들기
위 내용과 같은 것들을 반복한다.
셀을 만들고!
final class RankingFeatureCollectionViewCell: UICollectionViewCell {
static var height: CGFloat { 70.0 }
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.backgroundColor = .tertiarySystemGroupedBackground
imageView.layer.borderColor = UIColor.tertiaryLabel.cgColor
imageView.layer.borderWidth = 0.5
imageView.layer.cornerRadius = 7.0
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16.0, weight: .bold)
label.textColor = .label
label.numberOfLines = 2
return label
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 13.0, weight: .semibold)
label.textColor = .secondaryLabel
return label
}()
private lazy var downloadButton: UIButton = {
let button = UIButton()
button.setTitle("받기", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 13.0, weight: .bold)
button.backgroundColor = .secondarySystemBackground
button.layer.cornerRadius = 12.0
return button
}()
private lazy var inAppPurchaseInfoLabel: UILabel = {
let label = UILabel()
label.text = "앱 내 구입"
label.font = .systemFont(ofSize: 10.0, weight: .semibold)
label.textColor = .secondaryLabel
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupLayout()
titleLabel.text = "App Name"
descriptionLabel.text = "descrption"
inAppPurchaseInfoLabel.isHidden = [true, false].randomElement() ?? true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension RankingFeatureCollectionViewCell {
func setupLayout() {
[imageView,
titleLabel,
descriptionLabel,
downloadButton,
inAppPurchaseInfoLabel
].forEach { view in
addSubview(view)
}
imageView.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.top.equalToSuperview().inset(4.0)
make.bottom.equalToSuperview().inset(4.0)
make.width.equalTo(imageView.snp.height)
}
downloadButton.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
make.height.equalTo(24.0)
make.width.equalTo(50.0)
}
inAppPurchaseInfoLabel.snp.makeConstraints { make in
make.centerX.equalTo(downloadButton.snp.centerX)
make.top.equalTo(downloadButton.snp.bottom).offset(4.0)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalTo(imageView.snp.trailing).offset(8.0)
make.trailing.equalTo(downloadButton.snp.leading)
make.top.equalTo(imageView.snp.top).inset(8.0)
}
descriptionLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel.snp.leading)
make.trailing.equalTo(titleLabel.snp.trailing)
make.top.equalTo(titleLabel.snp.bottom).offset(4.0)
}
}
}
섹션뷰를 만들어서 붙여주고
final class RankingFeatureSectionView: UIView {
private let verticalCellCount: Int = 3 //더 많은 아이템을 보여주고 싶다면 늘려주자
private let separatorView = SeparatorView(frame: .zero)
private let cellIdentifier: String = "RankingFeatureCollectionViewCell"
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18.0, weight: .black)
label.text = "무료 게임 순위"
return label
}()
private lazy var showAllAppsButton: UIButton = {
let button = UIButton()
button.setTitle("모두 보기", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14.0, weight: .semibold)
return button
}()
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 32.0
layout.minimumInteritemSpacing = 0.0
layout.sectionInset = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0, right: 16.0)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isPagingEnabled = true
collectionView.backgroundColor = .systemBackground
collectionView.showsHorizontalScrollIndicator = false
collectionView.register(RankingFeatureCollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension RankingFeatureSectionView {
func setupLayout() {
[ titleLabel,
showAllAppsButton,
collectionView,
separatorView
].forEach { view in
addSubview(view)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(16.0)
make.top.equalToSuperview().inset(16.0)
make.trailing.equalTo(showAllAppsButton.snp.leading).offset(8.0)
}
showAllAppsButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(16.0)
make.bottom.equalTo(titleLabel.snp.bottom)
}
collectionView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(16.0)
make.height.equalTo(RankingFeatureCollectionViewCell.height * CGFloat(verticalCellCount))
}
separatorView.snp.makeConstraints { make in
make.bottom.leading.trailing.equalToSuperview()
make.top.equalTo(collectionView.snp.bottom).offset(16.0)
}
}
}
extension RankingFeatureSectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
8
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? RankingFeatureCollectionViewCell else { fatalError("wt...")}
return cell
}
}
extension RankingFeatureSectionView: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
CGSize(width: collectionView.frame.width - 32.0, height: RankingFeatureCollectionViewCell.height)
}
}
ViewController로 가서 랭킹 섹션도 붙여주자.
private var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 0.0
let featureSectionView = FeatureSectionView(frame: .zero)
let rankingFeatureSectionView = RankingFeatureSectionView(frame: .zero)
[featureSectionView,
rankingFeatureSectionView
].forEach { view in
stackView.addArrangedSubview(view)
}
return stackView
}()
잘 나온다.
다음엔 iOS13.0에서 소개된 CompositionalLayout으로 만드는 방법을 알아보자.