[Swift 톺아보기] 스크롤에 따른 애니메이션 만들기

kio·2022년 7월 20일
0

Swift

목록 보기
4/11

가장 중요한 오늘의 시연 영상부터 보고 오자!!!

왜 이런걸 만들었을까?

사실 지금 진행하는 프로젝트에서 스크롤에 따른 레이아웃의 변화가 있어야 했다.
하지만 아직 UI가 완벽히 만들어 지지 않았기 때문에 혼자 공부하는 시간동안 위와 같은 행동을 만들 수 있는 프로토콜을 만들어서 나중에 재사용하기로 하였다.

검색을 했을때 이렇다할 해결법이 없었는데...

이 것을 보고 하.. 직접만들어야겠구나 생각했다.

코드 뜯어보기

protocol EasyToAnimation {
    var targetHeight: Double { get set }
    var viewList: [UIView] { get set }
    var constranitList: [(UIView, Double) ->()] { get set }
    
    func move(contentOffset y: Double)
    func changeAlpha(_ y: Double) -> Double
    func aTob(start a: Double, end b: Double, _ y: Double) -> Double
}

별 것없다...

  • targetHeight : 스크롤로 이미지를 재배치시키는 최대 스크롤 값
    나는 노란 뷰를 다 덮을 때까지 레이아웃을 재배치 시키기 때문에 노란색뷰의 높이로 설정

  • viewList : 레이아웃을 재배치시킬 뷰들을 담을 list

  • constranitList : 해당 뷰를 어떤 레이아웃을 적용할지에 대한 클로져 배열

  • func move : contentOffset을 받아서 레이아웃 클로져를 실행시키는 함수

  • func changAlpha : 값에 따라 해당 뷰의 alpha 값을 1 ~ 0.5로 만드는 함수

  • func aTob : 시작 레이아웃 값부터 끝 레이아웃 값으로 변경시키는 함수

Protocol default implementaion

extension EasyToAnimation{
    
    func move(contentOffset y: Double) {
        let percentY = min ( 1, max ( y / self.targetHeight , 0))
        self.viewList.enumerated().forEach { idx, element in
            self.constranitList[idx](element, percentY)
            
        }
    }
    func changeAlpha(_ y: Double) -> Double{
        if (y == 1 || y == 0) { return 1 }
        else { return  y > 0.5 ? y : 1 - y }
    }
    
    func aTob(start a: Double, end b: Double, _ y: Double) -> Double {
         if (y == 0) { return a }
        if (y == 1) { return b }
        if (a > b){
            return a - ((a - b) * y )
        }
        else {
            return a + ((b - a) * y )
        }
    }
}

먼저 이실직고하자면 aTob는 레이아웃끼리 의존성이 있을때 잘 돌아가지 않습니다 추후에 업데이트하겠습니다.

func move

 func move(contentOffset y: Double) {
        let percentY = min ( 1, max ( y / self.targetHeight , 0))
        self.viewList.enumerated().forEach { idx, element in
            self.constranitList[idx](element, percentY)
        }
    }

우선 targetHeight으로 부터 얼마큼 즉 몇퍼센트 진행됬는지 0 ~ 1까지 나태내주는 percentY를 만들어주고
viewList에서 재배치할 view를 가져와서 해당 constraintList를 실행합니다.

이제 와서 생각하는건데 그냥 튜플로 한번에 담는게 오류가 적을 것 같다.
이번 경우에는 같은 인덱스에 같은 레이아웃 클로져가 들어있어서 꼭 인덱스를 맞춰야하는 단점이 있다.

func changeAlpha

func changeAlpha(_ y: Double) -> Double{
        if (y == 1 || y == 0) { return 1 }
        else { return  y > 0.5 ? y : 1 - y }
    }

이건 constraint 클로져에서 사용하는 함수라서 y라는 0 ~ 1까지의 값을 0.5 ~ 1로 뱉어준다.

func aTob

 func aTob(start a: Double, end b: Double, _ y: Double) -> Double {
        if (y == 0) { return a }
        if (y == 1) { return b }
        if (a > b){
            return a - ((a - b) * y )
        }
        else {
            return a + ((b - a) * y )
        }
}

이 함수는 시작 값부터 끝값을 주면 contentOffset.y의 퍼세트 값에 따라 레이아웃에 필요한 숫자를 뱉어주는 함수이다.

사용법

쨋든 이 프로젝트는 나중에 재활용할려고 쓰는 거기 때문에 사용법을 적어 놓는게 좋을 것같다.

 private func updataLayout(){
        
        viewList.append(mapView)
        constranitList.append({ view, y in
            view.snp.updateConstraints{
                $0.height.equalTo(self.aTob(start: 400, end: 0, y))
            }
        })
        
        viewList.append(imageView)
        constranitList.append({ view, y in
            view.alpha = self.changeAlpha(y)
            view.snp.updateConstraints{
                $0.top.leading.equalToSuperview().offset(self.aTob(start: 30, end: 0, y ))
                $0.width.equalTo( self.aTob(start: 100, end: self.view.frame.width, y) )
                $0.height.equalTo( self.aTob(start: 100, end: 300, y) )
            }
        })
}

우선 나는 mapview (노란색)과 imageView (회색)만 두개 보겠다.
mapview는 스크롤에 따라 높이를 줄여주었다.

왜냐하면 mapview는 그 높이에 따라 중앙에 내가 표시한 위치를 표시하는데 스크롤시 높이를 줄여주지 않으면 내가 찍은 포인트가 먼저 스크롤되어 위로 올라가버린는 상황이 발생한다.

 contentView.addSubview(mapView)
        mapView.snp.makeConstraints{
            $0.top.equalTo(view.safeAreaLayoutGuide)
            $0.centerX.equalToSuperview()
            $0.width.equalTo(scrollView.snp.width)
            $0.height.equalTo(400)
        }
 contentView.addSubview(infoView)
        infoView.snp.makeConstraints{
            $0.centerX.equalToSuperview()
            $0.top.equalTo(scrollView.snp.top).inset(self.mapView.frame.height)
            $0.width.equalToSuperview()
            $0.height.equalTo(1000)
        }

그래서 위와같이 mapview를 view상단에 고정하고, 아래에 파란 뷰를 그 노란색의 view의 높이만큼 스크롤 뷰에서 inset을 주었고,
scrollview에 bounce를 false로 해서 문제를 해결했다.

사용법으로 돌아가면 우선 재배치할 뷰를 viewListappend한다.

viewList.append(mapView)

그 후 재배치시 변할 레이아웃을 작성하여
constraintList에 append한다.

 constranitList.append({ view, y in
            view.snp.updateConstraints{
                $0.height.equalTo(self.aTob(start: 400, end: 0, y))
           	}
		})

여기서 인자 view는 재배치할 view, y는 진행된 상황 percentY이다.
이때 변화시킬 부분을 적고 aTob를 활용하여 시작 점과 끝점을 적어주면 자연스래 바뀌게 된다.

또 생각이 드는건 y는 자주쓰는데 그냥 저장 타입 프로퍼티로 해서 사용하는 것도 좋았을 것 같다는 생각이 든다.

하나만 더 보자면

viewList.append(imageView)
        constranitList.append({ view, y in
            view.alpha = self.changeAlpha(y)
            view.snp.updateConstraints{
                $0.top.leading.equalToSuperview().offset(self.aTob(start: 30, end: 0, y ))
                $0.width.equalTo( self.aTob(start: 100, end: self.view.frame.width, y) )
                $0.height.equalTo( self.aTob(start: 100, end: 300, y) )
            }
        })

다른점은 적용한게 많다는 점과
changeAlpha가 있다는 것이다.
changeAlpha는 진행도와 맞게 alpha 값을 바꿔주는 함수이다.

회고

블로그를 쓰는 것을 참 좋은 것같다.
내 코드를 설명할때, 내가 잘못했던 부분이나 더 좋았을거 같은 부분이 떠올라서이다.

그리고 언젠가 지금 만들고 있는 FastDevelope 시리즈를 라이브러리화할 생각이기에
이번 코드에서 고려하지않은 참조문제나, aTob가 다른 뷰와 레이아웃적인 의존성을 가질 때 잘 돌아가지 않는 점을 고쳐야할것 같다.

0개의 댓글