-
iOS) Appstore Transition 따라하기(1) - 기본 구성iOS 2020. 12. 2. 07:50
우리가 알게 모르게 익숙해져 있는 Appstore의 Transition을 구현 해보고자 한다.
생각 보다 복잡해 보인다...
애니메이션은 나중에 생각하고 화면의 구성부터 생각해보자.
- View와 ViewController
아래 tab bar와 기본적으로 메인 viewController, cell을 터치시 생성 되는 Viewcontroller.
collectionView와 collectionView cell로 구성하자.
TabBarController - main viewController - tableView - contentView
ㄴ DummyVC
시작 VC는 tab bar controller이다.
menu화면인 menuVC와 tabbar의 구색을 갖추기 위한 더미 VC를 하나 만들어준 뒤 tabbarVC에 넣어주자.
import UIKit import SnapKit class TabBarViewController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() print("Tabbar",#function) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) print("Tabbar",#function) let vc1 = AppStoreMenuViewController() let vcTabBarItem = UITabBarItem(title: "투데이", image: nil, tag: 0) let vc2 = SecondViewController() let vc2TabBarItem = UITabBarItem(title: "설정", image: nil, tag: 1) vc2TabBarItem.isEnabled = false vc1.tabBarItem = vcTabBarItem vc2.tabBarItem = vc2TabBarItem let controllers = [vc1,vc2] self.viewControllers = controllers } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) } override func viewWillLayoutSubviews() { GlobalConstants.safeAreaLayoutTop = view.safeAreaInsets.top } }
menuVC는 collectionView로 이루어져 있다.
collectionView의 cell에는 contentView라는 customView를 만들어서 나중에 재활용 할 수 있도록 한다.
import UIKit import SnapKit class AppStoreMenuViewController: UIViewController { var appStoreTransition = AppContentTransitionController() lazy var appCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() let cellWidth = view.frame.size.width * 0.9 let cellHeight = cellWidth * 1.2 var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) layout.itemSize = CGSize(width: cellWidth, height: cellHeight) layout.sectionInset.top = 10 collectionView.delaysContentTouches = false collectionView.dataSource = self collectionView.delegate = self collectionView.backgroundColor = .clear collectionView.register(AppCollectionViewCell.self, forCellWithReuseIdentifier: "AppCollectionViewCell") return collectionView }() override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .white setLayout() } func setLayout() { view.addSubview(appCollectionView) appCollectionView.snp.makeConstraints { (const) in const.top.equalTo(view.safeAreaLayoutGuide.snp.top) const.width.equalToSuperview() const.bottom.equalToSuperview() } } } extension AppStoreMenuViewController: UICollectionViewDelegate,UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return model.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = appCollectionView.dequeueReusableCell(withReuseIdentifier: "AppCollectionViewCell", for: indexPath) as! AppCollectionViewCell cell.fectchData(model: model[indexPath.row]) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let appContentVC = AppContentViewController() appStoreTransition.indexPath = indexPath appStoreTransition.superViewcontroller = appContentVC appContentVC.fetchData(model: model[indexPath.row]) appContentVC.modalPresentationStyle = .custom appContentVC.transitioningDelegate = appStoreTransition appContentVC.modalPresentationCapturesStatusBarAppearance = true self.present(appContentVC, animated: true, completion: nil) } }
import UIKit import SnapKit class AppCollectionViewCell: UICollectionViewCell { func resetTransform() { transform = .identity } lazy var appContentView: AppContentView = { var view = AppContentView(isContentView: false) view.layer.cornerRadius = 20 view.contentMode = .center return view }() override init(frame: CGRect) { super.init(frame: frame) self.configureCellLayout() } required init?(coder: NSCoder) { super.init(coder: coder) self.configureCellLayout() } func configureCellLayout() { self.clipsToBounds = true self.layer.cornerRadius = 20 contentView.addSubview(appContentView) appContentView.snp.makeConstraints { (const) in const.top.equalToSuperview() const.bottom.equalToSuperview() const.leading.equalToSuperview() const.trailing.equalToSuperview() } } func fectchData(model: AppContentModel) { appContentView.fetchDataForCell(image: model.image, subD: model.subDescription!, desc: model.description!) }
재사용을 위한 AppContentView는 cell에 사용 되는 View와 cell을 터치했을 때 보여지는 contentVC를 위한 View로 분기 시켜 구성 해준다.
import UIKit import SnapKit class AppContentView: UIView { //view 선언부 // MARK:- init init(isContentView: Bool, isTransition: Bool = false) { super.init(frame: .zero) // for reusable if isContentView { self.setLayoutForContentVC(isTransition: isTransition) } else { self.setLayoutForCollectionViewCell() } self.backgroundColor = .white scrollView.delegate = self } override init(frame: CGRect) { super.init(frame: frame) self.setLayoutForCollectionViewCell() } required init?(coder: NSCoder) { super.init(coder: coder) self.setLayoutForCollectionViewCell() } // MARK:- set Layout for CollectionViewCell func setLayoutForCollectionViewCell(image: UIImage? = nil, subDescription: String? = "", description: String? = "") { self.addSubview(imageView) self.addSubview(subDescriptionLabel) self.addSubview(descriptionLabel) imageView.snp.makeConstraints { (const) in const.centerX.equalToSuperview() const.centerY.equalToSuperview() const.width.equalToSuperview() const.height.equalToSuperview() } subDescriptionLabel.snp.makeConstraints { (const) in const.top.equalToSuperview().offset(15) const.leading.equalToSuperview().offset(15) const.width.equalTo(self.snp.width).multipliedBy(0.8) } descriptionLabel.snp.makeConstraints { (const) in const.top.equalTo(subDescriptionLabel.snp.bottom).offset(10) const.leading.equalToSuperview().offset(15) const.width.equalTo(self.snp.width).multipliedBy(0.8) } } // MARK:- set Layout for PresentedView func setLayoutForContentVC(isTransition: Bool = false) { self.addSubview(scrollView) scrollView.addSubview(imageView) scrollView.addSubview(subDescriptionLabel) scrollView.addSubview(descriptionLabel) scrollView.addSubview(closeButton) scrollView.addSubview(contentText) closeButton.addTarget(self, action: #selector(close), for: .touchUpInside) scrollView.snp.makeConstraints { (const) in const.centerX.equalTo(self.snp.centerX) const.centerY.equalToSuperview() const.width.equalToSuperview() const.height.equalToSuperview() } imageView.snp.makeConstraints { (const) in const.top.equalTo(scrollView.snp.top) const.centerX.equalTo(scrollView.snp.centerX) const.width.equalTo(scrollView.snp.width) const.height.equalTo(self.snp.width).multipliedBy(0.9 * 1.2).offset(GlobalConstants.safeAreaLayoutTop) } //iphone 모델 별 safeAreaLayout 적용 if !isTransition { subDescriptionLabel.snp.makeConstraints { (const) in const.top.equalTo(imageView.snp.top).offset(GlobalConstants.safeAreaLayoutTop) const.leading.equalTo(imageView.snp.leading).offset(20) const.width.equalTo(imageView.snp.width).multipliedBy(0.8) } descriptionLabel.snp.makeConstraints { (const) in const.top.equalTo(subDescriptionLabel.snp.bottom).offset(10) const.leading.equalTo(imageView.snp.leading).offset(20) const.width.equalTo(imageView.snp.width).multipliedBy(0.8) } } else { subDescriptionLabel.snp.makeConstraints { (const) in const.top.equalToSuperview().offset(15) const.leading.equalToSuperview().offset(15) const.width.equalTo(self.snp.width).multipliedBy(0.8) } descriptionLabel.snp.makeConstraints { (const) in const.top.equalTo(subDescriptionLabel.snp.bottom).offset(10) const.leading.equalToSuperview().offset(15) const.width.equalTo(self.snp.width).multipliedBy(0.8) } } closeButton.snp.makeConstraints { (const) in const.top.equalTo(self.snp.top).offset(20) const.right.equalTo(self.snp.right).offset(-20) const.width.equalTo(30) const.height.equalTo(30) } contentText.snp.makeConstraints { (const) in const.top.equalTo(imageView.snp.bottom) const.width.equalTo(scrollView.snp.width).multipliedBy(0.95) const.bottom.equalTo(scrollView.snp.bottom) const.height.equalTo(500) } } @objc func close() { NotificationCenter.default.post(name: .closeButton, object: nil) } // MARK:- Fetch Data For ContentVC func fetchDataForContentVC(image: UIImage?, subD: String, desc: String, content: String, contentView superViewFrame: CGRect, isTransition: Bool = false) { imageView.image = image subDescriptionLabel.text = subD contentText.text = content descriptionLabel.text = desc scrollView.scrollIndicatorInsets = UIEdgeInsets(top: superViewFrame.width * (0.9 * 1.2) + GlobalConstants.safeAreaLayoutTop, left: 0, bottom: 0, right: 0) let size = CGSize(width: 1000, height: 10000) let estimateSize = contentText.sizeThatFits(size) if !isTransition { contentText.snp.remakeConstraints { (const) in const.top.equalTo(superViewFrame.width * 0.9 * 1.2 + GlobalConstants.safeAreaLayoutTop) const.centerX.equalTo(scrollView.snp.centerX) const.width.equalTo(scrollView.snp.width).multipliedBy(0.95) const.bottom.equalTo(scrollView.snp.bottom) const.height.equalTo(estimateSize.height) } } else { contentText.snp.remakeConstraints { (const) in const.top.equalTo(imageView.snp.bottom) const.centerX.equalTo(scrollView.snp.centerX) const.width.equalTo(scrollView.snp.width).multipliedBy(0.95) const.bottom.equalTo(scrollView.snp.bottom) const.height.equalTo(estimateSize.height) } } } //MARK:- Fetch Data For CellView func fetchDataForCell(image: UIImage?, subD: String, desc: String) { imageView.image = image subDescriptionLabel.text = subD descriptionLabel.text = desc } }
collectionView의 cell이 구성되는 과정을 보면 처음 cell을 호출 하고 그 뒤에 데이터를 기존에 구성된 레이아웃에 넣어주는 형식이다.
이미지나 레이블 등의 위치와 크기는 고정 되어 있지만 아래의 메인컨텐츠의 경우 데이터의 크기에 따라 height가 결정된다.
따라서 데이터를 가져올 때 레이아웃을 재구성 해야 한다.
초기화시 height에 임의의 값을 넣어주고 fetchDataForContentVC에서 데이터를 가져 올 때 다시 레이아웃을 구성 해준다.
sizeTahtFits 함수를 보면
Asks the view to calculate and return the size that best fits the specified size라고 정의 되어 있다.
즉 알아서 뷰에 알맞는 사이즈를 리턴 해준다.
따라서 임의의 커다란 size를 선언 해주고 view에 적용 시켜주면 알맞는 크기의 사이즈를 리턴 해준다.
또한 transition 과정에서도 쓰일 수 있게 각 상황에 맞는 뷰 레이아웃을 구성 해준다.
//처음 레이아웃 구성시 contentText.snp.makeConstraints { (const) in const.top.equalTo(imageView.snp.bottom) const.width.equalTo(scrollView.snp.width).multipliedBy(0.95) const.bottom.equalTo(scrollView.snp.bottom) const.height.equalTo(500) } // 데이터를 가져올 때 func fetchDataForContentVC(image: UIImage?, subD: String, desc: String, content: String, contentView superViewFrame: CGRect, isTransition: Bool = false) { imageView.image = image subDescriptionLabel.text = subD contentText.text = content descriptionLabel.text = desc scrollView.scrollIndicatorInsets = UIEdgeInsets(top: superViewFrame.width * (0.9 * 1.2) + GlobalConstants.safeAreaLayoutTop, left: 0, bottom: 0, right: 0) let size = CGSize(width: 1000, height: 10000) let estimateSize = contentText.sizeThatFits(size) if !isTransition { contentText.snp.remakeConstraints { (const) in const.top.equalTo(superViewFrame.width * 0.9 * 1.2 + GlobalConstants.safeAreaLayoutTop) const.centerX.equalTo(scrollView.snp.centerX) const.width.equalTo(scrollView.snp.width).multipliedBy(0.95) const.bottom.equalTo(scrollView.snp.bottom) const.height.equalTo(estimateSize.height) } } else { contentText.snp.remakeConstraints { (const) in const.top.equalTo(imageView.snp.bottom) const.centerX.equalTo(scrollView.snp.centerX) const.width.equalTo(scrollView.snp.width).multipliedBy(0.95) const.bottom.equalTo(scrollView.snp.bottom) const.height.equalTo(estimateSize.height) } } }
transition 효과가 없는 appstore 틀이 완성 됐다.
- ContentViewcontroller
contentVC는 view - contentView( ScrollView - 여러 View들 )의 순으로 구성 되어 있다.
scrollView가 view를 감싸고 있고 하단의 textView가 스크롤 될 때
여기서 부터 살짝 이해가 안됐던 파트이다.
scrollView가 스크롤 되는 것인가? textView가 스크롤 되는 것 인가?
위로 드래그 할때 마지막 bouncing을 보니 textView일듯 싶은데 어째서 위의 이미지 뷰까지 같이 올라가는 거지?
------------------------------------------------------------------------------
첫번째 시도
contentView - imageView
ㄴ textView
실패했다. textView 안의 존재하는 scrollView를 다루기가 힘들다.
-------------------------------------------------------------------------------
두번째 시도
contentView - imageView
ㄴ scrollView - textView
scrollView의 contentOffset의 y값에 따라 imageView의 위치를 조정 해주려다 실패
-------------------------------------------------------------------------------
마지막 시도
contentView - scrollView - imageView
ㄴ textView
성공
-------------------------------------------------------------------------------
scrollViewDelegate를 채택하여 scrollView의 contentOffset을 이용한다.
scrollView의 imageView를 contentOffset.y가 0보다 작을 때, 즉 화면을 아래로 당길 때 고정시켜주고 이외에는 scrollView의 top에 종속 시켜준다.
scrollView의 indicator contentInsets 또한 textView의 top에 종속 된다.
extension AppContentView: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView.contentOffset.y < 0 { //scrollView.showsVerticalScrollIndicator = false } if scrollView.contentOffset.y < 0 { imageView.snp.remakeConstraints { (const) in const.top.equalTo(self.snp.top) const.centerX.equalToSuperview() const.width.equalToSuperview() const.height.equalTo(self.snp.width).multipliedBy(0.9 * 1.2).offset(GlobalConstants.safeAreaLayoutTop) } } else { imageView.snp.remakeConstraints { (const) in const.top.equalToSuperview() const.centerX.equalToSuperview() const.width.equalToSuperview() const.height.equalTo(self.snp.width).multipliedBy(0.9 * 1.2).offset(GlobalConstants.safeAreaLayoutTop) } } } }
포인트
- appContentView의 상황별 레이아웃 분기
- appcontentVC에서 scrollView와 textView의 스크롤
'iOS' 카테고리의 다른 글
iOS) Custom Present 이용시 ViewContoller Life Cycle 문제 (1) 2021.01.06 iOS) Appstore Transition 따라하기(2) - Present Transition (2) 2021.01.05 iOS) Singleton View와 viewController (feat. programmers iOS 실력체크) (0) 2020.10.24 iOS) Network -ATS (0) 2020.09.24 iOS) Data encoding to utf8 (0) 2020.08.31