Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- 네트워크
- 1일1알골
- 후기
- Swift
- widget
- 알고리즘
- 문법
- 운영체제
- composable architecture
- cs
- 컴퓨터그래픽스
- swift concurrency
- Protocol
- 위젯킷
- 멋쟁이사자처럼
- 백준
- 웹
- 멋사
- dispatchqueue
- widgetkit
- 영남대
- c++
- 리액트
- SwiftUI
- spritekit
- 대외활동
- 스유
- uikit
- 스위프트
- TCA
Archives
- Today
- Total
맛동산이
CustomView) Two Button Slider 구현하기 UIkit 본문
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' 카테고리의 다른 글
Swift) deepLink, Universal Link 처리하기 (1) | 2024.09.17 |
---|---|
Swift) Error type에 대해서 (1) | 2024.08.25 |
Swift) OptionSet, 그리고 이를 이용한 ActionSheet를 사용해보자 (0) | 2024.08.23 |
Swift) Delegate, Completionhandler 를 async await을 통해서 리팩토링하는법(by. continuation) (1) | 2024.06.06 |
Swift) Decodable, custom Decoder 복잡한 데이터 모델을 나누는 방법에 대해서 (2) | 2024.06.06 |