본문 바로가기

난 iOS개발자/iOS

AppStore 화면 만들어보기

728x90

이러한 화면은 너무나 익숙할 것이다.
근데 이런화면 어떻게 만들지..?

다양한 방법이 떠오를 수 있다.
이번에는 ScrollView와 StackView 그리고 CollectionView의 조합으로 위와 같은 화면을 구성해 보고자 한다.

일단 필요한 라이브러리가 있다. 굳이 사용하지 않아도 되지만 편하게 UI구성을 하고 싶다면 SanpKit을 넣어주자.
방법은 아래 링크 참고
2021.10.29 - [앱 개발 도움 링크/Libraries(Github...)] - [iOS] SnapKit 사용법

[iOS] SnapKit 사용법

🤔왜 이걸 쓰지? 개발 시 스토리보드를 사용하여 화면의 UI를 구성할 수도 있지만 Autolayout을 코드로 작성하는 방법도 있다. NSLayoutConstraint를 직접 지정해주며 하나하나 제약을 적용해 주는 방법

greate-future.tistory.com


ㅇㅋ 시작


화면이 어떻게 그려질지 대강 한 번 떠올려 보자.

그림이 좀 허접하지만 이런 구조로 만들면 좋을것 같다.
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으로 만드는 방법을 알아보자.

728x90

'난 iOS개발자 > iOS' 카테고리의 다른 글

[iOS] Sandbox  (0) 2023.10.30
[iOS] UserDefaults 알아보기  (0) 2023.10.30
HomeBrew설치 그리고 실패  (0) 2022.06.20
COW(Copy On Write)  (0) 2022.05.28
Global DispatchQueue의 QoSClass  (0) 2022.05.10