ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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의 스크롤

    댓글

Designed by Tistory.