CustomView) Two Button Slider 구현하기 UIkit

2024. 10. 17. 19:38·앱/Swift

Two button slider를 구한현 예제가 많지 않고, 거기에 이번에 회사 스팩중에 하나가 슬라이더 안에 밑에 격자가 들어가거나, 위에 점이 들어가거나 하는 커스텀이 많이 들어갔다.

그래서 나처럼 구현할 사람이 있을것 같아서 블로그에 남겨본다.

먼저 편의성을 위해서 snapkit과 then을 사용한 코드 임을 감안! 해주세요!

슬라이더 예시

 

 

 

//
//  SliderView.swift
//  twoHandledSlider
//
//  Created by 최진용 on 10/12/24.
//

import UIKit
import Then
import SnapKit

protocol SliderViewDelegate: AnyObject {
    func sliderView(_ sender: SliderView, changedValue value: Int)
}

final class SliderView: UIView {
    
    weak var delegate: SliderViewDelegate?
    var value: Int = 1 {
        didSet {
            delegate?.sliderView(self, changedValue: value)
        }
    }
    
    private let trackView = UIView().then {
        $0.backgroundColor = .systemGray5
    }
    
    private lazy var leftThumbView = UIView().then {
        $0.backgroundColor = .systemBackground
        $0.isUserInteractionEnabled = true
        let gesture = UIPanGestureRecognizer(target: self, action: #selector(handleLeftPan))
        $0.addGestureRecognizer(gesture)
        $0.layer.shadowColor = UIColor.gray.cgColor
        $0.layer.shadowOffset = .init(width: 3, height: 3)
        $0.layer.shadowRadius = 8
        $0.layer.shadowOpacity = 0.8
    }
    
    private lazy var rightThumbView = UIView().then {
        $0.backgroundColor = .systemBackground
        $0.isUserInteractionEnabled = true
        let gesture = UIPanGestureRecognizer(target: self, action: #selector(handleRightPan))
        $0.addGestureRecognizer(gesture)
        $0.layer.shadowColor = UIColor.gray.cgColor
        $0.layer.shadowOffset = .init(width: 3, height: 3)
        $0.layer.shadowRadius = 8
        $0.layer.shadowOpacity = 0.8
    }
    
    private let fillTrackView = UIView().then {
        $0.backgroundColor = .systemBlue
    }
    
    private var dividers: [UIView] = []
    
    private var maxValue: Int
    private var touchLeftBeganPosX: CGFloat?
    private var touchRightBeganPosX: CGFloat?
    private var didLayoutSubViews: Bool = false
    
    private let thumbSize: CGFloat = 30
    
    init(maxValue: Int) {
        if maxValue < 1 {
            self.maxValue = 1
        }
        else if maxValue > 20 {
            self.maxValue = 20
        }
        else{
            self.maxValue = maxValue
        }
        super.init(frame: .zero)
        
        render()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        if !didLayoutSubViews {
            makeDividerAndLayout()
            leftThumbView.layer.cornerRadius = leftThumbView.frame.width / 2
            leftThumbView.layer.shadowPath = UIBezierPath(
                roundedRect: leftThumbView.bounds,
                cornerRadius: leftThumbView.layer.cornerRadius
            ).cgPath
            
            rightThumbView.layer.cornerRadius = rightThumbView.frame.width / 2
            rightThumbView.layer.shadowPath = UIBezierPath(
                roundedRect: rightThumbView.bounds,
                cornerRadius: rightThumbView.layer.cornerRadius
            ).cgPath
        }
    }
    
    private func render() {
        [trackView, fillTrackView, leftThumbView, rightThumbView].forEach(addSubview)
        
        trackView.snp.makeConstraints {
            $0.left.right.equalToSuperview().inset(10)
            $0.verticalEdges.equalToSuperview().inset(10)
        }
        leftThumbView.snp.makeConstraints {
            $0.centerY.equalTo(trackView)
            $0.left.equalTo(trackView).offset(-(thumbSize / 2))
            $0.size.equalTo(thumbSize)
        }
        rightThumbView.snp.makeConstraints {
            $0.centerY.equalTo(trackView)
            $0.right.equalTo(trackView).offset(thumbSize / 2)
            $0.size.equalTo(thumbSize)
        }
        fillTrackView.snp.makeConstraints {
            $0.left.equalTo(leftThumbView)
            $0.right.equalTo(rightThumbView)
            $0.top.bottom.equalTo(trackView)
        }
    }
    
    private func makeDividerAndLayout() {
        let unitWidth = trackView.frame.width / CGFloat(maxValue - 1)
        
        for i in 0..<maxValue {
            let dividerPosX = unitWidth * CGFloat(i)
            let divider = makeDivider()
            
            trackView.addSubview(divider)
            divider.snp.makeConstraints {
                $0.top.equalTo(trackView.snp.bottom).offset(6)
                $0.left.equalTo(trackView).offset(dividerPosX - 4)
                $0.width.equalTo(1)
                $0.height.equalTo(4)
            }
        }
        
        didLayoutSubViews.toggle()
    }
    
    private func makeDivider() -> UIView {
        let divider = UIView()
        divider.backgroundColor = .black
        divider.clipsToBounds = true
        dividers.append(divider)
        return divider
    }
    
    @objc 
    func handleLeftPan(_ recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translation(in: leftThumbView)
        
        if recognizer.state == .began {
            // 팬 제스쳐가 시작된 x좌표 저장
            touchLeftBeganPosX = leftThumbView.frame.minX
        }
        if recognizer.state == .changed {
            guard let startX = self.touchLeftBeganPosX else { return }
            
            var offSet = startX + translation.x // 시작지점 + 제스쳐 거리 = 현재 제스쳐 좌표
            if offSet < 0 || offSet > trackView.frame.width { return } // 제스쳐가 trackView의 범위를 벗어나는 경우 무시
            let unitWidth = trackView.frame.width / CGFloat(maxValue - 1) // 1단위 너비
            
            // value = 반올림(현재 제스쳐 좌표 / 1단위의 크기) -> 슬라이더의 값이 변할 때마다 똑똑 끊기는 효과를 주기 위해
            let newValue = round(offSet / unitWidth)
            offSet = unitWidth * newValue - (thumbSize / 2)
            
            guard (rightThumbView.frame.minX - unitWidth) > offSet else { return }
            
            leftThumbView.snp.updateConstraints {
                $0.left.equalTo(trackView).offset(offSet)
            }
            fillTrackView.snp.updateConstraints {
                $0.left.equalTo(leftThumbView)
                $0.right.equalTo(rightThumbView)
            }
        }
    }

    @objc
    func handleRightPan(_ recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translation(in: rightThumbView)
        
        if recognizer.state == .began {
            // 팬 제스쳐가 시작된 x좌표 저장
            touchRightBeganPosX = rightThumbView.frame.maxX
        }
        if recognizer.state == .changed {
            guard let startX = self.touchRightBeganPosX else { return }
            print(startX)
            var offSet = startX + translation.x // 시작지점 + 제스쳐 거리 = 현재 제스쳐 좌표
            print(offSet)
            if offSet < 0 || offSet > trackView.frame.width { return } // 제스쳐가 trackView의 범위를 벗어나는 경우 무시
            let unitWidth = trackView.frame.width / CGFloat(maxValue - 1) // 1단위 너비
            
            // value = 반올림(현재 제스쳐 좌표 / 1단위의 크기) -> 슬라이더의 값이 변할 때마다 똑똑 끊기는 효과를 주기 위해
            let newValue = round(offSet / unitWidth)
            //오른쪽 기준(trackView.frame.width)에서 뺄 값
            offSet = trackView.frame.width - (unitWidth * newValue + (thumbSize / 2))
//            (unitWidth * newValue + (thumbSize / 2))
            guard (leftThumbView.frame.minX + unitWidth) < (unitWidth * newValue + (thumbSize / 2)) else { return }
            
            rightThumbView.snp.updateConstraints {
                $0.right.equalTo(trackView).offset(-offSet)
            }
            fillTrackView.snp.updateConstraints {
                $0.left.equalTo(leftThumbView)
                $0.right.equalTo(rightThumbView)
            }
        }
    }

}

 

 

여기에서 delegate부분은 직접 구현해주시며 됩니다!

반응형
저작자표시 비영리 변경금지 (새창열림)

'앱 > Swift' 카테고리의 다른 글

Moya Mock Data 사용하기(feat. test Double)  (0) 2025.01.31
reactorkit에서 testcode 작성 하는법(feat. nimble)  (0) 2025.01.31
Swift) deepLink, Universal Link 처리하기  (1) 2024.09.17
Swift) Error type에 대해서  (1) 2024.08.25
Swift) OptionSet, 그리고 이를 이용한 ActionSheet를 사용해보자  (0) 2024.08.23
'앱/Swift' 카테고리의 다른 글
  • Moya Mock Data 사용하기(feat. test Double)
  • reactorkit에서 testcode 작성 하는법(feat. nimble)
  • Swift) deepLink, Universal Link 처리하기
  • Swift) Error type에 대해서
진ddang
진ddang
안녕하세요 진땅의 개발자 블로그 입니다. 피드백은 환영입니다. 깃헙 : https://github.com/it794613
    반응형
  • 진ddang
    맛동산이
    진ddang
  • 전체
    오늘
    어제
    • 분류 전체보기 (202)
      • 일기 그리고 목표 (1)
      • 웹 (20)
        • 리액트 (19)
      • 앱 (118)
        • Swift 문법 (15)
        • Swift (68)
        • SwiftUI (32)
        • 리액트 네이티브 (3)
      • CS (30)
        • 컴퓨터그래픽스 (8)
        • 운영체제 (6)
        • 네트워크 (16)
      • 알고리즘 (13)
        • 백준 (12)
        • 프로그래머스 (1)
      • 대외활동 (7)
        • ict한이음(2022.04) (2)
        • 멋쟁이 사자처럼 (5)
        • Apple Developer Academy (0)
      • 다양한 내용들 (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    TCA
    Protocol
    widgetkit
    spritekit
    swift concurrency
    웹
    스유
    알고리즘
    멋사
    컴퓨터그래픽스
    Swift
    c++
    cs
    백준
    멋쟁이사자처럼
    ReactorKit
    uikit
    네트워크
    리액트
    composable architecture
    후기
    dispatchqueue
    스위프트
    문법
    영남대
    운영체제
    SwiftUI
    위젯킷
    대외활동
    widget
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
진ddang
CustomView) Two Button Slider 구현하기 UIkit
상단으로

티스토리툴바