ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [iOS, Swift] (TagCalendar 컴포넌트 1) 이동하는 뷰 만들기
    iOS/UI 컴포넌트 2023. 6. 1. 11:40
    반응형

    Swift에서 드래그해서 이동하는 뷰를 만드는 방법을 소개하려고 합니다.

    처음에 이러한 디자인을 구현하려고 생각했을 때, 막막했었는데 작은 것 하나하나 조립해서 결국에 원하던 기능을 만들 수 있게 되었습니다.

     

    먼저, 팀에서 만들었던 앱에 있던 디자인 및 구현한 화면을 소개하겠습니다.

     

    이 뷰는 사용자가 직접 뷰를 드래그하여 다른 위치로 이동할 수 있습니다.

     

    구현할 기능

    1.  사용자가 뷰를 길게 누릅니다. 이때 뷰는 이동 상태로 변경되고, 이 상태는 뷰의 그림자나 기타 시각적 요소를 통해 표시됩니다.
    2. 사용자가 손가락을 움직이면, 뷰가 같이 이동하며 새로운 위치로 이동합니다. 이 때 새로운 위치는 다른 뷰와 일정간격이 띄어지게 합니다.
    3. 만약 뷰가 다른 위치로 이동하려고 하는 곳에 이미 다른 뷰가 있다면, 뷰는 원래 위치로 돌아갑니다.

     

    위의 단계는 사용자가 뷰를 자유롭게 이동시킬 수 있게 해주며, 이는 앱의 사용성을 크게 향상시킵니다.

     

    Chap1. 드래그할 뷰를 MovingView로 이름 짓고 만들겠습니다.

     

    - 전체 소스코드 -

    import UIKit
    
    enum MovingViewState {
        case began
        case ended
    }
    
    class MovingView: UIView {
        
        var state: MovingViewState = .began {
            didSet {
                handleStateChange()
            }
        }
        
        private var dragStartLocation: CGPoint = .zero
        private var originalPosition: CGPoint = .zero
        private let dashedBorder = CAShapeLayer()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupView()
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setupView()
        }
        
        private func setupView() {
            layer.cornerRadius = 10
            
            setupGesture()
        }
        
        private func setupGesture() {
            let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
            addGestureRecognizer(longPressGesture)
        }
        
        @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
            state = .began
            switch gesture.state {
            case .began:
                state = .began
                dragStartLocation = gesture.location(in: self)
                originalPosition = frame.origin
            case .changed:
                updatePosition(with: gesture)
            case .ended, .cancelled:
                state = .ended
            default:
                break
            }
        }
        
        private func updatePosition(with gesture: UILongPressGestureRecognizer) {
            let locationInSuperview = gesture.location(in: superview)
            frame.origin = CGPoint(x: locationInSuperview.x - dragStartLocation.x,
                                    y: locationInSuperview.y - dragStartLocation.y)
        }
        
        private func handleStateChange() {
            switch state {
            case .began:
                alpha = 0.5
                addDashedBorder()
            case .ended:
                alpha = 1.0
                removeDashedBorder()
            }
        }
        
        private func addDashedBorder() {
            let path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius)
            dashedBorder.path = path.cgPath
            dashedBorder.fillColor = nil
            dashedBorder.strokeColor = UIColor.lightGray.cgColor
            dashedBorder.lineDashPattern = [4, 4]
            dashedBorder.lineWidth = 2
            layer.addSublayer(dashedBorder)
        }
        
        private func removeDashedBorder() {
            dashedBorder.removeFromSuperlayer()
        }
        
    }

     

    MovingView의 상태값을 Enum 타입을 만들어 상태에 따라 뷰의 디자인이 달라지도록 했습니다. (이동 중일 때는 Alpha 값이 0.5, 그리고 경계선에 대시 값을 주어 사용자가 이동 중이라는 것을 알 수 있게끔 했습니다.)

     

    Chap2. 제스쳐 이벤트 추가

    @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
        switch gesture.state {
        case .began:
            state = .began
            dragStartLocation = gesture.location(in: self)
            originalPosition = frame.origin
        case .changed:
            updatePosition(with: gesture)
        case .ended, .cancelled:
            state = .ended
        default:
            break
        }
    }
    
    private func updatePosition(with gesture: UILongPressGestureRecognizer) {
        let locationInSuperview = gesture.location(in: superview)
        frame.origin = CGPoint(x: locationInSuperview.x - dragStartLocation.x,
                                y: locationInSuperview.y - dragStartLocation.y)
    }

    뷰가 longPress 이벤트를 발생하면 호출되는 코드입니다.

     

    LongPress 이벤트 함수

    1. LongPress 이벤트가 처음 시작했을 때 시작 지점과 본래 뷰의 위치를 저장합니다.
    2. LongPress를 누르면서 손가락을 이동시켰을 때 updatePosition 함수를 실행합니다.
    3. updatePosition 함수에서는 뷰의 위치를 움직일 때마다 업데이트합니다.
    4. LongPress 이벤트가 종료가 되면 상태값을 ended로 바꾸어 뷰의 디자인을 원래 상태로 되돌립니다.

     

     

    실행 화면

     

    UI 작업을 하면서 애플에서 제공하는 TableView 또는 CollectionView만 사용해왔었는데 앱을 만들면서 기획된 디자인이 프레임워크만이 제공하는 것만을 사용할 수 없을 때 막막했는데 컴포넌트를 만들면서 기본적인 UIView를 커스텀하여 원하는 기능을 만들게 되어 재밌었습니다.

     

    이후로 뷰가 겹쳤을 때 일어나는 충돌 방지와 스크롤 뷰안에서 MovingView를 일정한 간격으로 이동할 수 있게끔 하는 기능을 구현한 것을 포스팅하겠습니다.

     

    전체소스 https://github.com/tuche24/MovingView

    반응형
Designed by Tistory.