diff --git a/Animation-iOS/Animation-iOS.xcodeproj/project.pbxproj b/Animation-iOS/Animation-iOS.xcodeproj/project.pbxproj index 299b4fb..7198a70 100644 --- a/Animation-iOS/Animation-iOS.xcodeproj/project.pbxproj +++ b/Animation-iOS/Animation-iOS.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + CA4C8DD32EF02B2D004FDBB5 /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = CA4C8DD22EF02B2D004FDBB5 /* Then */; }; CA54AA7D2EB8B1040073AAE1 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = CA54AA7C2EB8B1040073AAE1 /* SnapKit */; }; CA54AA862EB8D3300073AAE1 /* Toast in Frameworks */ = {isa = PBXBuildFile; productRef = CA54AA852EB8D3300073AAE1 /* Toast */; }; /* End PBXBuildFile section */ @@ -42,6 +43,7 @@ buildActionMask = 2147483647; files = ( CA54AA862EB8D3300073AAE1 /* Toast in Frameworks */, + CA4C8DD32EF02B2D004FDBB5 /* Then in Frameworks */, CA54AA7D2EB8B1040073AAE1 /* SnapKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -87,6 +89,7 @@ packageProductDependencies = ( CA54AA7C2EB8B1040073AAE1 /* SnapKit */, CA54AA852EB8D3300073AAE1 /* Toast */, + CA4C8DD22EF02B2D004FDBB5 /* Then */, ); productName = "Animation-iOS"; productReference = CA54AA5F2EB8AF930073AAE1 /* Animation-iOS.app */; @@ -119,6 +122,7 @@ packageReferences = ( CA54AA7B2EB8B1040073AAE1 /* XCRemoteSwiftPackageReference "SnapKit" */, CA54AA842EB8D3300073AAE1 /* XCRemoteSwiftPackageReference "Toast-Swift" */, + CA4C8DD12EF02B2D004FDBB5 /* XCRemoteSwiftPackageReference "Then" */, ); preferredProjectObjectVersion = 77; productRefGroup = CA54AA602EB8AF930073AAE1 /* Products */; @@ -358,6 +362,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + CA4C8DD12EF02B2D004FDBB5 /* XCRemoteSwiftPackageReference "Then" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/devxoul/Then"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; CA54AA7B2EB8B1040073AAE1 /* XCRemoteSwiftPackageReference "SnapKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SnapKit/SnapKit"; @@ -377,6 +389,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + CA4C8DD22EF02B2D004FDBB5 /* Then */ = { + isa = XCSwiftPackageProductDependency; + package = CA4C8DD12EF02B2D004FDBB5 /* XCRemoteSwiftPackageReference "Then" */; + productName = Then; + }; CA54AA7C2EB8B1040073AAE1 /* SnapKit */ = { isa = XCSwiftPackageProductDependency; package = CA54AA7B2EB8B1040073AAE1 /* XCRemoteSwiftPackageReference "SnapKit" */; diff --git a/Animation-iOS/Animation-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Animation-iOS/Animation-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 14d0f38..071ca5b 100644 --- a/Animation-iOS/Animation-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Animation-iOS/Animation-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "cb5b05002b0761c69de568e3e660143e44b28e2148fee70f5b2daa2d9e973adc", + "originHash" : "4c1344dfa466c2b7976e9fddc84cf1194ba4eeec87b38909c25f58d1a8c04538", "pins" : [ { "identity" : "snapkit", @@ -10,6 +10,15 @@ "version" : "5.7.1" } }, + { + "identity" : "then", + "kind" : "remoteSourceControl", + "location" : "https://github.com/devxoul/Then", + "state" : { + "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a", + "version" : "3.0.0" + } + }, { "identity" : "toast-swift", "kind" : "remoteSourceControl", diff --git a/Animation-iOS/Animation-iOS/Assets.xcassets/kirby0.imageset/.DS_Store b/Animation-iOS/Animation-iOS/Assets.xcassets/kirby0.imageset/.DS_Store new file mode 100644 index 0000000..8ca1507 Binary files /dev/null and b/Animation-iOS/Animation-iOS/Assets.xcassets/kirby0.imageset/.DS_Store differ diff --git a/Animation-iOS/Animation-iOS/ProgressBarView.swift b/Animation-iOS/Animation-iOS/ProgressBarView.swift new file mode 100644 index 0000000..e9871d6 --- /dev/null +++ b/Animation-iOS/Animation-iOS/ProgressBarView.swift @@ -0,0 +1,79 @@ +import UIKit + +final class ProgressBarView: UIView { + + // MARK: - Properties + + private var circleLayer = CAShapeLayer() + private var progressLayer = CAShapeLayer() + + + private var startPoint = CGFloat(3 * Double.pi / 4) + + private var endPoint = CGFloat(Double.pi / 4) + + // MARK: - Drawing + + override func draw(_ rect: CGRect) { + createCircularPath() + } + + // MARK: - Private Methods + + + private func createCircularPath() { + self.backgroundColor = .white + + + let path = UIBezierPath( + arcCenter: CGPoint(x: self.frame.width / 2, + y: self.frame.height / 2), + radius: (frame.size.height - 10) / 2, + startAngle: startPoint, + endAngle: endPoint, + clockwise: true + ) + + + circleLayer.path = path.cgPath + circleLayer.fillColor = UIColor.clear.cgColor + circleLayer.lineCap = .round + circleLayer.lineWidth = 10 + circleLayer.strokeEnd = 1 + circleLayer.strokeColor = UIColor.black.withAlphaComponent(0.4).cgColor + layer.addSublayer(circleLayer) + + + progressLayer.path = path.cgPath + progressLayer.fillColor = UIColor.clear.cgColor + progressLayer.lineCap = .round + progressLayer.lineWidth = 10 + progressLayer.strokeEnd = 0 + progressLayer.strokeColor = UIColor.systemRed.cgColor + layer.addSublayer(progressLayer) + } + + // MARK: - Public Methods + + + func progressAnimation(duration: TimeInterval, value: Double) { + + let circularProgressAnimation = CABasicAnimation(keyPath: "strokeEnd") + circularProgressAnimation.duration = duration + + + circularProgressAnimation.toValue = value + + + circularProgressAnimation.fillMode = .forwards + circularProgressAnimation.isRemovedOnCompletion = false + + progressLayer.add(circularProgressAnimation, forKey: "progressAnim") + } + + + func resetProgress() { + progressLayer.removeAnimation(forKey: "progressAnim") + progressLayer.strokeEnd = 0 + } +} diff --git a/Animation-iOS/Animation-iOS/ProgressBarViewController.swift b/Animation-iOS/Animation-iOS/ProgressBarViewController.swift new file mode 100644 index 0000000..c119ca1 --- /dev/null +++ b/Animation-iOS/Animation-iOS/ProgressBarViewController.swift @@ -0,0 +1,127 @@ +import UIKit +import SnapKit +import Then + +final class ProgressBarViewController: UIViewController { + + // MARK: - UI Components + + private let fullCircleView = FullCircleProgressView() + private let semiCircleView = SemiCircleProgressView() + private let linearView = LinearProgressView() + + private let label1 = UILabel() + private let label2 = UILabel() + private let label3 = UILabel() + + private let startButton = UIButton(type: .system) + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + setStyle() + setHierarchy() + setLayout() + setTarget() + } + + // MARK: - Setup + + private func setStyle() { + view.backgroundColor = .white + title = "Progress Bar" + + label1.do { + $0.text = "완전한 원" + $0.font = .systemFont(ofSize: 14) + $0.textAlignment = .center + } + + label2.do { + $0.text = "반원" + $0.font = .systemFont(ofSize: 14) + $0.textAlignment = .center + } + + label3.do { + $0.text = "직선 바" + $0.font = .systemFont(ofSize: 14) + $0.textAlignment = .center + } + + startButton.do { + $0.setTitle("Start All", for: .normal) + $0.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) + $0.backgroundColor = .systemBlue + $0.setTitleColor(.white, for: .normal) + $0.layer.cornerRadius = 12 + } + } + + private func setHierarchy() { + [fullCircleView, semiCircleView, linearView, + label1, label2, label3, startButton].forEach { + view.addSubview($0) + } + } + + private func setLayout() { + fullCircleView.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide).offset(40) + $0.centerX.equalToSuperview() + $0.size.equalTo(120) + } + + label1.snp.makeConstraints { + $0.top.equalTo(fullCircleView.snp.bottom).offset(8) + $0.centerX.equalToSuperview() + } + + semiCircleView.snp.makeConstraints { + $0.top.equalTo(label1.snp.bottom).offset(30) + $0.centerX.equalToSuperview() + $0.size.equalTo(120) + } + + label2.snp.makeConstraints { + $0.top.equalTo(semiCircleView.snp.bottom).offset(8) + $0.centerX.equalToSuperview() + } + + linearView.snp.makeConstraints { + $0.top.equalTo(label2.snp.bottom).offset(30) + $0.leading.trailing.equalToSuperview().inset(40) + $0.height.equalTo(30) + } + + label3.snp.makeConstraints { + $0.top.equalTo(linearView.snp.bottom).offset(8) + $0.centerX.equalToSuperview() + } + + startButton.snp.makeConstraints { + $0.top.equalTo(label3.snp.bottom).offset(40) + $0.centerX.equalToSuperview() + $0.width.equalTo(150) + $0.height.equalTo(50) + } + } + + private func setTarget() { + startButton.addTarget(self, action: #selector(startButtonTapped), for: .touchUpInside) + } + + // MARK: - Actions + + @objc private func startButtonTapped() { + fullCircleView.progressAnimation(duration: 2.0, value: 0.75) + semiCircleView.progressAnimation(duration: 2.0, value: 0.75) + linearView.progressAnimation(duration: 2.0, value: 0.75) + } +} + +#Preview { + ProgressBarViewController() +} diff --git a/Animation-iOS/Animation-iOS/ProgressViews.swift b/Animation-iOS/Animation-iOS/ProgressViews.swift new file mode 100644 index 0000000..21ff06a --- /dev/null +++ b/Animation-iOS/Animation-iOS/ProgressViews.swift @@ -0,0 +1,138 @@ +import UIKit + +// MARK: - 1. 완전한 원 (360도) +final class FullCircleProgressView: UIView { + + private var circleLayer = CAShapeLayer() + private var progressLayer = CAShapeLayer() + + override func draw(_ rect: CGRect) { + createCircularPath() + } + + private func createCircularPath() { + backgroundColor = .white + + let path = UIBezierPath( + arcCenter: CGPoint(x: frame.width / 2, y: frame.height / 2), + radius: (frame.size.height - 10) / 2, + startAngle: -(.pi / 2), // 12시 방향에서 시작 + endAngle: .pi * 1.5, // 한 바퀴 돌아서 12시 방향으로 + clockwise: true + ) + + circleLayer.path = path.cgPath + circleLayer.fillColor = UIColor.clear.cgColor + circleLayer.lineCap = .round + circleLayer.lineWidth = 10 + circleLayer.strokeColor = UIColor.black.withAlphaComponent(0.2).cgColor + layer.addSublayer(circleLayer) + + progressLayer.path = path.cgPath + progressLayer.fillColor = UIColor.clear.cgColor + progressLayer.lineCap = .round + progressLayer.lineWidth = 10 + progressLayer.strokeEnd = 0 + progressLayer.strokeColor = UIColor.systemBlue.cgColor + layer.addSublayer(progressLayer) + } + + func progressAnimation(duration: TimeInterval, value: Double) { + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.duration = duration + animation.toValue = value + animation.fillMode = .forwards + animation.isRemovedOnCompletion = false + progressLayer.add(animation, forKey: "progressAnim") + } +} + +// MARK: - 2. 반원 (위쪽) +final class SemiCircleProgressView: UIView { + + private var circleLayer = CAShapeLayer() + private var progressLayer = CAShapeLayer() + + override func draw(_ rect: CGRect) { + createCircularPath() + } + + private func createCircularPath() { + backgroundColor = .white + + let path = UIBezierPath( + arcCenter: CGPoint(x: frame.width / 2, y: frame.height / 2), + radius: (frame.size.height - 10) / 2, + startAngle: .pi, // 9시 방향에서 시작 + endAngle: 0, // 3시 방향에서 끝 + clockwise: true + ) + + circleLayer.path = path.cgPath + circleLayer.fillColor = UIColor.clear.cgColor + circleLayer.lineCap = .round + circleLayer.lineWidth = 10 + circleLayer.strokeColor = UIColor.black.withAlphaComponent(0.2).cgColor + layer.addSublayer(circleLayer) + + progressLayer.path = path.cgPath + progressLayer.fillColor = UIColor.clear.cgColor + progressLayer.lineCap = .round + progressLayer.lineWidth = 10 + progressLayer.strokeEnd = 0 + progressLayer.strokeColor = UIColor.systemGreen.cgColor + layer.addSublayer(progressLayer) + } + + func progressAnimation(duration: TimeInterval, value: Double) { + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.duration = duration + animation.toValue = value + animation.fillMode = .forwards + animation.isRemovedOnCompletion = false + progressLayer.add(animation, forKey: "progressAnim") + } +} + +// MARK: - 3. 직선 바 +final class LinearProgressView: UIView { + + private var backgroundLayer = CAShapeLayer() + private var progressLayer = CAShapeLayer() + + override func draw(_ rect: CGRect) { + createLinearPath() + } + + private func createLinearPath() { + backgroundColor = .white + + let path = UIBezierPath() + path.move(to: CGPoint(x: 10, y: frame.height / 2)) + path.addLine(to: CGPoint(x: frame.width - 10, y: frame.height / 2)) + + backgroundLayer.path = path.cgPath + backgroundLayer.fillColor = UIColor.clear.cgColor + backgroundLayer.lineCap = .round + backgroundLayer.lineWidth = 10 + backgroundLayer.strokeColor = UIColor.black.withAlphaComponent(0.2).cgColor + layer.addSublayer(backgroundLayer) + + progressLayer.path = path.cgPath + progressLayer.fillColor = UIColor.clear.cgColor + progressLayer.lineCap = .round + progressLayer.lineWidth = 10 + progressLayer.strokeEnd = 0 + progressLayer.strokeColor = UIColor.systemOrange.cgColor + layer.addSublayer(progressLayer) + } + + func progressAnimation(duration: TimeInterval, value: Double) { + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.duration = duration + animation.toValue = value + animation.fillMode = .forwards + animation.isRemovedOnCompletion = false + progressLayer.add(animation, forKey: "progressAnim") + } +} diff --git a/Animation-iOS/Animation-iOS/SceneDelegate.swift b/Animation-iOS/Animation-iOS/SceneDelegate.swift index c8f8fe8..19d0fcf 100644 --- a/Animation-iOS/Animation-iOS/SceneDelegate.swift +++ b/Animation-iOS/Animation-iOS/SceneDelegate.swift @@ -19,7 +19,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // 2. let window = UIWindow(windowScene: windowScene) // 3. - let vc = UINavigationController(rootViewController: SegmentedControlViewController()) + let vc = UINavigationController(rootViewController: SkeletonViewController()) // 4. window.rootViewController = vc // 5. diff --git a/Animation-iOS/Animation-iOS/SkeletonViewController.swift b/Animation-iOS/Animation-iOS/SkeletonViewController.swift new file mode 100644 index 0000000..f9730ef --- /dev/null +++ b/Animation-iOS/Animation-iOS/SkeletonViewController.swift @@ -0,0 +1,96 @@ +import UIKit +import SnapKit +import Then + +final class SkeletonViewController: UIViewController { + + // MARK: - UI Components + + private let imageView = UIView() + private let startButton = UIButton(type: .system) + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + setStyle() + setHierarchy() + setLayout() + setTarget() + } + + // MARK: - Setup + + private func setStyle() { + view.backgroundColor = .white + title = "Skeleton Animation!!!!!" + + imageView.do { + $0.backgroundColor = .gray + $0.layer.cornerRadius = 8 + } + + startButton.do { + $0.setTitle("시작작작", for: .normal) + $0.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) + $0.backgroundColor = .systemBlue + $0.setTitleColor(.white, for: .normal) + $0.layer.cornerRadius = 12 + } + } + + private func setHierarchy() { + view.addSubview(imageView) + view.addSubview(startButton) + } + + private func setLayout() { + imageView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.size.equalTo(150) + } + + startButton.snp.makeConstraints { + $0.top.equalTo(imageView.snp.bottom).offset(60) + $0.centerX.equalToSuperview() + $0.width.equalTo(120) + $0.height.equalTo(50) + } + } + + private func setTarget() { + startButton.addTarget(self, action: #selector(startButtonTapped), for: .touchUpInside) + } + + // MARK: - Actions + + @objc private func startButtonTapped() { + skeletonAnimate() + } + + // MARK: - Animation + + private func skeletonAnimate() { + imageView.backgroundColor = .gray + + UIView.animateKeyframes(withDuration: 4, delay: 0) { + UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) { + self.imageView.alpha = 0.4 + } + UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) { + self.imageView.alpha = 1 + } + } completion: { _ in + self.bind() + } + } + + private func bind() { + imageView.backgroundColor = .systemPink + } +} + +#Preview { + SkeletonViewController() +}