Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

별점 커스텀 뷰 #119

Merged
merged 3 commits into from
Sep 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions Soyeon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@
1D7C3243268767E100D7F2C9 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 1D7C3242268767E100D7F2C9 /* AppAuth */; };
1D7C3245268767E100D7F2C9 /* AppAuthCore in Frameworks */ = {isa = PBXBuildFile; productRef = 1D7C3244268767E100D7F2C9 /* AppAuthCore */; };
1D7C32582687680100D7F2C9 /* AuthInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7C324F2687680100D7F2C9 /* AuthInfo.swift */; };
1D910ED526CA580200CB1EF5 /* RatingStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D910ED426CA580200CB1EF5 /* RatingStackView.swift */; };
1D910EDB26CA58A600CB1EF5 /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D910EDA26CA58A600CB1EF5 /* RatingView.swift */; };
1DA32034262C757900ED564A /* LoadSignupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA32032262C757900ED564A /* LoadSignupView.swift */; };
1DA32046262C75D900ED564A /* LoadSignupViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA32045262C75D900ED564A /* LoadSignupViewData.swift */; };
1DBA13C425D440E400979BB2 /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 1DBA13C325D440E400979BB2 /* Moya */; };
Expand Down Expand Up @@ -368,6 +370,8 @@
1D5B63C625B4824F00A5321C /* StartSplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartSplashViewController.swift; sourceTree = "<group>"; };
1D64300425F4CBCE00A84D45 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
1D7C324F2687680100D7F2C9 /* AuthInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthInfo.swift; sourceTree = "<group>"; };
1D910ED426CA580200CB1EF5 /* RatingStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingStackView.swift; sourceTree = "<group>"; };
1D910EDA26CA58A600CB1EF5 /* RatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingView.swift; sourceTree = "<group>"; };
1DA32032262C757900ED564A /* LoadSignupView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadSignupView.swift; sourceTree = "<group>"; };
1DA32045262C75D900ED564A /* LoadSignupViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadSignupViewData.swift; sourceTree = "<group>"; };
1DC9DF4525AAEE7600822E99 /* PageViewType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageViewType.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1204,6 +1208,15 @@
path = AppAuth;
sourceTree = "<group>";
};
1D910ED126CA57CC00CB1EF5 /* RatingView */ = {
isa = PBXGroup;
children = (
1D910ED426CA580200CB1EF5 /* RatingStackView.swift */,
1D910EDA26CA58A600CB1EF5 /* RatingView.swift */,
);
path = RatingView;
sourceTree = "<group>";
};
1DBA14DD25D46BE000979BB2 /* Signup */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1341,6 +1354,7 @@
D80D664A25BDBA1C002F47B9 /* Common */ = {
isa = PBXGroup;
children = (
1D910ED126CA57CC00CB1EF5 /* RatingView */,
008FB5B926885CBA00F4EF54 /* Manager */,
1DD0CCB6261A11AC003E6347 /* UserDefault */,
0A83E4232602549D001823C0 /* SoyeonToast */,
Expand Down Expand Up @@ -1786,6 +1800,7 @@
1D19088425A9F77E008210D1 /* AgreementInteractor.swift in Sources */,
0A4E515D25EAB4E400616C07 /* WriteProfileDefualtButton.swift in Sources */,
009B02AF25B47E9E00753847 /* LoginViewController.swift in Sources */,
1D910EDB26CA58A600CB1EF5 /* RatingView.swift in Sources */,
D8354D7D25B4348E00C9C531 /* NewAccountRouter.swift in Sources */,
0A4E515F25EAB4E400616C07 /* WriteProfileAlertAction.swift in Sources */,
0AC21E0F2613FDE400AD0635 /* AdditionalViewController.swift in Sources */,
Expand Down Expand Up @@ -1885,15 +1900,16 @@
0A9C2A552699D6340042692D /* KKOIDAuth.swift in Sources */,
0A4E515825EAB4E400616C07 /* JobAlertItem.swift in Sources */,
0AC21E162613FDE400AD0635 /* AdditionalRouter.swift in Sources */,
1D910ED526CA580200CB1EF5 /* RatingStackView.swift in Sources */,
D8354D7125B4347000C9C531 /* NewAccountConfigurator.swift in Sources */,
008FB5B426871F5200F4EF54 /* ImagePickerManager.swift in Sources */,
1D19087525A9F77E008210D1 /* DetailContents.swift in Sources */,
1D19087E25A9F77E008210D1 /* AgreementPresenter.swift in Sources */,
00CF092E25E25CA70077700A /* ProviderManager.swift in Sources */,
000CEB2D25B47FF400F64A28 /* UIView+.swift in Sources */,
1D5B63C725B4824F00A5321C /* StartSplashViewController.swift in Sources */,
1D1908C925AA1645008210D1 /* PagingView.swift in Sources */,
00E295E7266AB4DD00DB69CE /* UIImage+.swift in Sources */,
1D1908C925AA1645008210D1 /* PagingView.swift in Sources */,
00E295E7266AB4DD00DB69CE /* UIImage+.swift in Sources */,
0A9C2A63269A0C3E0042692D /* SYDefault.swift in Sources */,
D8795C42258E6A2100DF4D64 /* SceneDelegate.swift in Sources */,
1DD0CCBC261A11F1003E6347 /* SoyeonUserDefault.swift in Sources */,
Expand Down
153 changes: 153 additions & 0 deletions Soyeon/Common/RatingView/RatingStackView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// RatingStackView.swift
// Soyeon
//
// Created by 박은비 on 2021/08/16.
// Copyright © 2021 ludus. All rights reserved.
//

import UIKit

protocol RatingStackDelegate: AnyObject {
func didChangeIndex(_ index: Int)
}

class RatingStackView: UIStackView {

private var count: Int = 0
private var imageSubViews: [UIImageView] = []
private var subViewArea: CGFloat = 0.0

private var initalPointX: CGFloat = 0.0 // Gesture Property

private var index: Int = -1 {
willSet {
delegate?.didChangeIndex(newValue)
setHighlight(until: newValue)
}
}

weak var delegate: RatingStackDelegate?

override init(frame: CGRect) {
super.init(frame: .zero)

translatesAutoresizingMaskIntoConstraints = false
isUserInteractionEnabled = true
axis = .horizontal

configureTapGesture()
configurePanGesture()
}

required init(coder: NSCoder) {
super.init(coder: coder)
}

convenience init(arrangedImageSubviews imageViews: [UIImageView]) {
self.init(frame: .zero)

for view in imageViews {
self.addArrangedSubview(view)
}

self.imageSubViews = imageViews
self.count = imageViews.count

}

override func draw(_ rect: CGRect) {
super.draw(rect)

calcuratingArea()
}

override func insertArrangedSubview(_ view: UIView, at stackIndex: Int) { }
override func removeArrangedSubview(_ view: UIView) { }

override func addArrangedSubview(_ view: UIView) {
if imageSubViews.count == 0 {
super.addArrangedSubview(view)
}
}

private func calcuratingArea() {
guard let firstView = arrangedSubviews.first else { return }
subViewArea = firstView.bounds.width + spacing
}

private func numberHorizontal(of pointX: CGFloat) -> Int {
guard 0 < pointX else { return -1 }
let number = Int((pointX + spacing / 2.0) / subViewArea)

guard number < arrangedSubviews.count else { return -1 }

return number
}

func changeIndex(to index: Int) {
guard self.index != index, 0 <= index else {
return
}

self.index = min(index, count-1)
}

private func setHighlight(until i: Int) {
imageSubViews[0...i].forEach { $0.isHighlighted = true }
imageSubViews[i+1..<count].forEach { $0.isHighlighted = false }
}

}

// MARK: - Gesture
private extension RatingStackView {
private func configureTapGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(didTapGesture(_:)))
addGestureRecognizer(tap)
}

private func configurePanGesture() {
let pan = UIPanGestureRecognizer(target: self, action: #selector(didPanGesture(_:)))
addGestureRecognizer(pan)
}

@objc
private func didTapGesture(_ sender: UITapGestureRecognizer) {
guard let view = sender.view as? UIStackView else {
return
}

let pointX = sender.location(in: view).x

let index = numberHorizontal(of: pointX)
changeIndex(to: index)

}

@objc
private func didPanGesture(_ sender: UIPanGestureRecognizer) {
guard let view = sender.view as? UIStackView else {
return
}

switch sender.state {
case .began:
initalPointX = sender.location(in: view).x
case .changed:

// panning 위치가 ImageView의 반 이상이 넘어갔을때 하이라이팅
let translationX = sender.translation(in: view).x
let positionX = initalPointX + translationX - (subViewArea / 2.0)

let index = numberHorizontal(of: positionX)
changeIndex(to: index)

case .ended:
initalPointX = 0.0
default:
break
}
}
}

150 changes: 150 additions & 0 deletions Soyeon/Common/RatingView/RatingView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// RatingView.swift
// Soyeon
//
// Created by 박은비 on 2021/08/16.
// Copyright © 2021 ludus. All rights reserved.
//

import UIKit

protocol RatingViewDelegate: AnyObject {
func didChangeIndex(_ index: Int)
}

final class RatingView: UIView {

private var stackView: RatingStackView!

weak var delegate: RatingViewDelegate?

private var imageViewAttributes: RatingImageViewsAttributes = .init()

private var stackViewAttributes: RatingStackViewAttributes = .init()

override func awakeFromNib() {
super.awakeFromNib()

setupLayout()

let imageViews = initImageViews(attributes: imageViewAttributes)

setupStackView(with: imageViews, attributes: stackViewAttributes)

addSubview(stackView)
setupStackViewConstraint()
}

private func setupLayout() {

translatesAutoresizingMaskIntoConstraints = false

tintColor = imageViewAttributes.highlightColor
}

private func initImageViews(attributes: RatingImageViewsAttributes) -> [UIImageView] {
let imageName = attributes.imageName
let highlightColor = attributes.highlightColor
let count = attributes.count

let imageViews: [UIImageView] = (0..<count).map { _ in
let image = UIImage(named: imageName)
let highlight = image?.imageColor(to: highlightColor)
let iv = UIImageView(image: image,
highlightedImage: highlight)
return iv
}

return imageViews
}

private func setupStackView(with imageViews: [UIImageView], attributes: RatingStackViewAttributes) {
stackView = RatingStackView(arrangedImageSubviews: imageViews)
stackView.spacing = attributes.spacing

stackView.delegate = self

stackView.changeIndex(to: attributes.index)
}

private func setupStackViewConstraint() {

let constraints = [
stackView.leftAnchor.constraint(equalTo: leftAnchor),
stackView.rightAnchor.constraint(equalTo: rightAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
]

NSLayoutConstraint.activate(constraints)
}
}

extension RatingView: RatingStackDelegate {
func didChangeIndex(_ index: Int) {
delegate?.didChangeIndex(index)
}
}

// MARK: - Stack View
extension RatingView {
/// user defined runtime attributes
struct RatingStackViewAttributes {
var index: Int
var spacing: CGFloat

init() {
self.index = 0
self.spacing = 13.0
}
}

@objc var index: NSNumber {
get { NSNumber(value: stackViewAttributes.index) }
set { stackViewAttributes.index = newValue.intValue }
}

@objc var spacing: NSNumber {
get { NSNumber(value: Float(stackViewAttributes.spacing)) }
set { stackViewAttributes.spacing = CGFloat(newValue.floatValue) }
}
}

// MARK: - Rating Image View
extension RatingView {
/// user defined runtime attributes
struct RatingImageViewsAttributes {
var imageName: String
var highlightColor: UIColor
var count: Int

init() {
self.imageName = ""
self.highlightColor = UIColor.blue
self.count = 5
}
}

@objc var imageName: NSString {
get { NSString(string: imageViewAttributes.imageName) }
set { imageViewAttributes.imageName = String(newValue) }
}

@objc var highlightColor: UIColor {
get { imageViewAttributes.highlightColor }
set { imageViewAttributes.highlightColor = newValue }
}

@objc var count: NSNumber {
get { NSNumber(value: imageViewAttributes.count) }
set { imageViewAttributes.count = newValue.intValue }
}

}

fileprivate extension UIImage {
func imageColor(to color: UIColor) -> UIImage? {
let image = withRenderingMode(.alwaysTemplate)
return image.withTintColor(color)
}
}
2 changes: 1 addition & 1 deletion Soyeon/Intro/Base.lproj/Intro.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="37Y-sB-tDD" userLabel="Contents View">
<rect key="frame" x="0.0" y="0.0" width="414" height="544"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="f69-8Z-k2e" userLabel="PagingView" customClass="PagingView" customModule="Soyeon" customModuleProvider="target">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="f69-8Z-k2e" customClass="PagingView" customModule="Soyeon" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="474"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
Expand Down
Loading