App Shop 프로젝트

sanghee·2021년 10월 7일
0

👏iOS 스터디

목록 보기
10/10

소개

App Store 클론 프로젝트입니다.

기간

2021.10.02 ~ 10.06

기술

  • Swift
  • MVP Pattern

라이브러리

  • SnapKit

깃허브

https://github.com/sanghee-dev/App-Shop
https://github.com/della-padula/YappUltraHardPractice/tree/main/Sanghee/Practice3

특징

  • UICollectionView을 사용하였습니다.
  • 커스텀 애니메이터를 구현하였습니다.

화면

메인 화면, 상세 화면

  • 메인 화면은 스크롤이 가능하며 피드를 클릭시 상세 화면으로 이동합니다.
  • 상세 화면에서는 클릭한 피드의 세부 내용을 확인할 수 있습니다.

애니메이션

  • 메인 화면에서 상세 화면으로, 상세 화면에서 메인 화면으로 이동시 애니메이션으로 화면이 전환됩니다.
  • 메인 화면에서 피드를 클릭하면 상세 화면으로 이동합니다. 상세 화면에서는 cancel 버튼을 클릭하거나 화면을 아래로 당기는 경우에 메인 화면으로 이동합니다.
  • 메인 화면에서의 피드가 상세 화면의 위에 위치됩니다.
  • 메인 화면에서의 뷰에 대한 피드의 y값을 이용해 다시 상세 화면에서 메인 화면으로 이동시에도 자연스럽게 애니메이션이 보여집니다.


코드 설명

더욱 자세한 코드는 깃허브에서 확인해주세요.

Model

MainUnit, DetailUnit

MainUnit은 메인 화면의 단위 모델이다. DetailUnit은 디테일 정보들이다.

struct MainUnit {
    let title: String
    let subTitle: String
    let emoji: String
    let backgroundColor: UIColor
    let detailUnits: [DetailUnit]
}
struct DetailUnit {
    let title: String
    let emoji: String
    let paragraph: String
}

Function

PopAnimator

이 애니메이셔는 duration동안 애니메이션을 실행한다. padding값은 메인 화면에서 좌우 간격값이다. cPointY는 메인 화면에서 선택한 셀의 뷰에 대한 중심점의 Y 위치값이다. 메인 화면에서 디테일 화면으로 전환될 때는 animateTransition함수에서 toView 코드가 실행되며, 디테일 화면에서 메인 화면으로 전환될 때는 fromView 코드가 실행된다. fromView의 코드에서 디테일 화면에서 메인 화면으로 이동할때에는 cPointY값을 활용하여 메인 화면에서의 위치로 다시 이동시킨다.

class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    let duration: TimeInterval = 1.0
    let padding: CGFloat = 16
    var cPointY: CGFloat = 0
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView

        let toView = transitionContext.view(forKey: .to)
        let fromView = transitionContext.view(forKey: .from)
        
        if let toView = toView {
            containerView.addSubview(toView)
            containerView.bringSubviewToFront(toView)
            
            toView.clipsToBounds = true
            toView.layer.cornerRadius = 12
            toView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
            
            UIView.animate(withDuration: duration,
                           delay: 0,
                           usingSpringWithDamping: 0.5,
                           initialSpringVelocity: 0.1,
                           animations: {
                            toView.transform = .identity
                            toView.frame = containerView.frame
                           },
                           completion: { _ in
                            transitionContext.completeTransition(true)
                           })
        }
        if let fromView = fromView {
            containerView.addSubview(fromView)
            containerView.bringSubviewToFront(fromView)
            
            fromView.clipsToBounds = true
            fromView.layer.cornerRadius = 12
            fromView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
            
            let width = containerView.frame.width - self.padding * 2

            UIView.animate(withDuration: duration,
                           delay: 0,
                           usingSpringWithDamping: 0.6,
                           initialSpringVelocity: 0.1,
                           animations: {
                            fromView.transform = .identity
                            fromView.frame = CGRect(x: self.padding, y: self.cPointY - (width / 2), width: width, height: width)
                           },
                           completion: { _ in
                            transitionContext.completeTransition(true)
                           })
        }
    }
}

MainViewController

cPointY

컬렉션뷰에서 셀을 클릭하면 해당 셀의 뷰에 대한 중심점 y위치값을 저장한다. getCollectionViewItemCPoint 함수는 컬렉션뷰의 index를 보내면 컬렉션뷰의 상위뷰에 대한 중심점을 반환한다. 이 반환된 중심점의 y값을 cPointY에 저장한다.

그리고 화면이 dismiss되었을 때 이 값을 위의 animator 코드의 cPointY 변수에 값을 저장한다.

class MainViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
    private var cPointY: CGFloat = 0
    ...
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let detailVC = DetailViewController()
        detailVC.mainUnit = mainUnits[indexPath.row]
        detailVC.modalPresentationStyle = .overFullScreen
        detailVC.transitioningDelegate = self
        
        // 선택한 item y값 저장
        cPointY = getCollectionViewItemCPoint(indexPath: indexPath).y
        
        self.present(detailVC, animated: true, completion: nil)
    }
    
    // 선택한 item y값 얻기
    private func getCollectionViewItemCPoint(indexPath: IndexPath) -> CGPoint {
        let attributes = collectionView.layoutAttributesForItem(at: indexPath)
        let cPoint = collectionView.convert(attributes?.center ?? CGPoint(), to: collectionView.superview)
        
        return cPoint
    }
}
extension MainViewController: UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return animator
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        animator.cPointY = cPointY
        return animator
    }
}

Presenter

MainPresenter

MainView에서 구현할 메소드가 포함된 Protocol을 선언한다. 그리고 MainViewPresenter에서 구현할 메소드가 포함된 Protocol을 선언한다. MainPresenter는 현재 날짜와 정보 데이터를 가져오는 역할을 수행한다.

protocol MainView: AnyObject {
    func setHeader()
}

protocol MainViewPresenter {
    func getMainUnits()
}

class MainPresenter: MainViewPresenter {
    var mainUnits: [MainUnit] = []
    var currentDateString: String = ""
    
    init() {
        getMainUnits()
        getDateString()
    }
    
    func getDateString() {
        ...
    }
    
    func getMainUnits() {
        mainUnits = [
            MainUnit(...)
        ]
    }
}
profile
👩‍💻

0개의 댓글