관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Custom Switch (커스텀 스위치) 구현 방법, touchesEnded 본문

UI 컴포넌트 (swift)

[iOS - swift] Custom Switch (커스텀 스위치) 구현 방법, touchesEnded

jake-kim 2022. 6. 2. 23:52

커스텀 스위치

사용하는 쪽

class ViewController: UIViewController {
  private let jkSwitchOne: JKSwitch = {
    let view = JKSwitch()
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()
  
  private let jkSwitchTwo: JKSwitch = {
    let view = JKSwitch()
    view.barTintColor = .red
    view.circleColor = .orange
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    self.view.addSubview(self.jkSwitchOne)
    self.view.addSubview(self.jkSwitchTwo)
    
    NSLayoutConstraint.activate([
      self.jkSwitchOne.heightAnchor.constraint(equalToConstant: 70),
      self.jkSwitchOne.widthAnchor.constraint(equalToConstant: 200),
      self.jkSwitchOne.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 150),
      self.jkSwitchOne.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
    ])
    NSLayoutConstraint.activate([
      self.jkSwitchTwo.heightAnchor.constraint(equalToConstant: 50),
      self.jkSwitchTwo.widthAnchor.constraint(equalToConstant: 100),
      self.jkSwitchTwo.topAnchor.constraint(equalTo: self.jkSwitchOne.bottomAnchor, constant: 80),
      self.jkSwitchTwo.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
    ])
  }
}

구현 아이디어

  • 동그라미 뷰(circleView)와 배경 뷰(barView)를 정의
    • barView안에 circleView를 넣고 circleView의 left, top, bottom을 barView와 같도록 레이아웃 초기화
    • 배경 뷰와 동그라미 뷰 모두 cornerRadius값을 height의 반으로 설정하여 round 뷰 형태로 유지
    • roundableView 만드는 방법은 이전 포스팅 글 참고
  • 터치가 발생할때마다 동그란 circleView를 오토레이아웃을 이용하여 오른쪽 or 왼쪽으로 이동하도록 구현
    • 터치가 발생한 이벤트는 touchesBegan() 메소드를 통해 알수 있고, 이 메소드가 불릴때 레이아웃 업데이트

구현

  • circleView와 barView 둘다 roundableView 형태이므로 RoundableView 정의
final class RoundableView: UIView {
  override func layoutSubviews() {
    super.layoutSubviews()
    self.layer.cornerRadius = self.frame.height / 2
  }
}
  • JKSwitch 클래스 준비
    • UIControl를 서브클래싱하여 사용
import UIKit

final class JKSwitch: UIControl {

}

// UIControl를 서브클래싱한 이유
// JKSwitch에서 switch값이 바뀌는 부분에 아래 코드를 넣으면 사용하는쪽에서 addTarget으로 값이 변경될때 이벤트처리가 가능
self.sendActions(for: .valueChanged)

// 사용하는쪽
view.addTarget(self, action: #selector(changeSwitchValue), for: .valueChanged)
  • UI 2개와 오토레이아웃 변경시 duration을 상수로 준비
    • circleView에는 입체감을 주기 위해서 shadow 처리도 추가
  private enum Constant {
    static let duration = 0.25
  }
  
  // MARK: UI
  private let barView: RoundableView = {
    let view = RoundableView()
    view.backgroundColor = .gray
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()
  private let circleView: RoundableView = {
    let view = RoundableView()
    view.backgroundColor = .white
    view.layer.shadowOffset = CGSize(width: 0, height: 3)
    view.layer.shadowColor = UIColor.black.cgColor
    view.layer.shadowOpacity = 0.3
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()
  • 외부에서 접근할수 있는 프로퍼티 준비
  var barColor = UIColor.gray {
    didSet { self.barView.backgroundColor = self.barColor }
  }
  var barTintColor = UIColor.green
  var circleColor = UIColor.white {
    didSet { self.circleView.backgroundColor = self.circleColor }
  }
  • 스윗칭될때마다 circleView의 오토레이아웃을 변경해주어야 하므로 isActive를 false로 시키고 다시 정의하기 위해서 프로퍼티로 준비
private var circleViewConstraints = [NSLayoutConstraint]()
  • 외부에서 스윗칭 할 수 있는 isOn 프로퍼티 선언
    • self.sendActions(for: .valueChanged) 코드를 통해 사용하는쪽에서 valueChanged 이벤트 처리가 가능하게끔 정의
    • 이곳에서 스위치 상태에 따라 배경색과 autolayout을 애니메이션을 통해 변경
var isOn = false {
  didSet {
    self.sendActions(for: .valueChanged)
    
    UIView.animate(
      withDuration: Constant.duration,
      delay: 0,
      options: .curveEaseInOut,
      animations: {
        self.barView.backgroundColor = self.isOn ? self.barTintColor : self.barColor
        
        self.circleViewConstraints.forEach { $0.isActive = false }
        self.circleViewConstraints.removeAll()
        
        if self.isOn {
          self.circleViewConstraints = [
            self.circleView.rightAnchor.constraint(equalTo: self.barView.rightAnchor),
            self.circleView.bottomAnchor.constraint(equalTo: self.barView.bottomAnchor),
            self.circleView.topAnchor.constraint(equalTo: self.barView.topAnchor),
            self.circleView.heightAnchor.constraint(equalTo: self.barView.heightAnchor),
            self.circleView.widthAnchor.constraint(equalTo: self.barView.heightAnchor)
          ]
        } else {
          self.circleViewConstraints = [
            self.circleView.leftAnchor.constraint(equalTo: self.barView.leftAnchor),
            self.circleView.bottomAnchor.constraint(equalTo: self.barView.bottomAnchor),
            self.circleView.topAnchor.constraint(equalTo: self.barView.topAnchor),
            self.circleView.heightAnchor.constraint(equalTo: self.barView.heightAnchor),
            self.circleView.widthAnchor.constraint(equalTo: self.barView.heightAnchor)
          ]
        }
        
        NSLayoutConstraint.activate(self.circleViewConstraints)
        self.layoutIfNeeded()
      },
      completion: nil
    )
  }
}
  • 레이아웃 초기화
override init(frame: CGRect) {
  super.init(frame: frame)
  
  self.addSubview(self.barView)
  self.barView.addSubview(self.circleView)
  
  NSLayoutConstraint.activate([
    self.barView.leftAnchor.constraint(equalTo: self.leftAnchor),
    self.barView.rightAnchor.constraint(equalTo: self.rightAnchor),
    self.barView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
    self.barView.topAnchor.constraint(equalTo: self.topAnchor),
  ])
  self.circleViewConstraints = [
    self.circleView.leftAnchor.constraint(equalTo: self.barView.leftAnchor),
    self.circleView.bottomAnchor.constraint(equalTo: self.barView.bottomAnchor),
    self.circleView.topAnchor.constraint(equalTo: self.barView.topAnchor),
    self.circleView.heightAnchor.constraint(equalTo: self.barView.heightAnchor),
    self.circleView.widthAnchor.constraint(equalTo: self.barView.heightAnchor)
  ]
  NSLayoutConstraint.activate(self.circleViewConstraints)
}
  • 터치가 끝난 경우, isOn값을 변경해주는 코드를 touchesEnded에 적용
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  super.touchesEnded(touches, with: event)
  self.isOn = !self.isOn
}

* 전체 코드: https://github.com/JK0369/ExSwitch

Comments