Skip to content

Commit

Permalink
[refactor] BottleMotionViewController 리팩토링 #303
Browse files Browse the repository at this point in the history
  • Loading branch information
skkimeo committed Jul 1, 2023
1 parent 524c34d commit eb5ae49
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 191 deletions.
20 changes: 4 additions & 16 deletions Happiggy-bank/Happiggy-bank.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@
A491018727D6F37C0012DFDD /* NSObject+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = A491018627D6F37C0012DFDD /* NSObject+Name.swift */; };
A491018D27D7358B0012DFDD /* Bottle+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = A491018C27D7358B0012DFDD /* Bottle+CoreDataClass.swift */; };
A491018F27D735920012DFDD /* Note+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = A491018E27D735920012DFDD /* Note+CoreDataClass.swift */; };
A499318127BF3A38009FF5A8 /* BottleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A499318027BF3A38009FF5A8 /* BottleViewModel.swift */; };
A499318327BF5158009FF5A8 /* BottleNoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A499318227BF5158009FF5A8 /* BottleNoteView.swift */; };
A49931A527BFD20E009FF5A8 /* UIImage+AssetImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = A49931A427BFD20E009FF5A8 /* UIImage+AssetImages.swift */; };
A49A8C212952F54900D5468A /* DebugPrint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A49A8C202952F54900D5468A /* DebugPrint.swift */; };
Expand Down Expand Up @@ -164,7 +163,7 @@
A89CBEDD27CBDEA2005549F6 /* BottleListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A89CBEDC27CBDEA2005549F6 /* BottleListViewController.swift */; };
A89FF2F12959902C00BF0E1B /* HomeTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A89FF2F02959902C00BF0E1B /* HomeTabViewModel.swift */; };
A8BD833327BE104900E0DE41 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BD833227BE104900E0DE41 /* HomeViewController.swift */; };
A8BD833527BE106C00E0DE41 /* BottleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BD833427BE106C00E0DE41 /* BottleViewController.swift */; };
A8BD833527BE106C00E0DE41 /* BottleMotionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BD833427BE106C00E0DE41 /* BottleMotionViewController.swift */; };
A8BD833F27BE30B400E0DE41 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BD833E27BE30B400E0DE41 /* Constants.swift */; };
A8BD834727BE337900E0DE41 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BD834627BE337900E0DE41 /* HomeView.swift */; };
A8C972032952C88D00CB59F9 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = A8C972022952C88D00CB59F9 /* SnapKit */; };
Expand Down Expand Up @@ -282,7 +281,6 @@
A491018627D6F37C0012DFDD /* NSObject+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSObject+Name.swift"; sourceTree = "<group>"; };
A491018C27D7358B0012DFDD /* Bottle+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bottle+CoreDataClass.swift"; sourceTree = "<group>"; };
A491018E27D735920012DFDD /* Note+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Note+CoreDataClass.swift"; sourceTree = "<group>"; };
A499318027BF3A38009FF5A8 /* BottleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleViewModel.swift; sourceTree = "<group>"; };
A499318227BF5158009FF5A8 /* BottleNoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleNoteView.swift; sourceTree = "<group>"; };
A49931A427BFD20E009FF5A8 /* UIImage+AssetImages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+AssetImages.swift"; sourceTree = "<group>"; };
A49A8C202952F54900D5468A /* DebugPrint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPrint.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -346,7 +344,7 @@
A89CBEDC27CBDEA2005549F6 /* BottleListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleListViewController.swift; sourceTree = "<group>"; };
A89FF2F02959902C00BF0E1B /* HomeTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTabViewModel.swift; sourceTree = "<group>"; };
A8BD833227BE104900E0DE41 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
A8BD833427BE106C00E0DE41 /* BottleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleViewController.swift; sourceTree = "<group>"; };
A8BD833427BE106C00E0DE41 /* BottleMotionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleMotionViewController.swift; sourceTree = "<group>"; };
A8BD833E27BE30B400E0DE41 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
A8BD834627BE337900E0DE41 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
A8D58DAF299286A700FD52A1 /* NewBottleDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewBottleDateView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -676,7 +674,6 @@
isa = PBXGroup;
children = (
A896FBE12966B41800A3400B /* UI */,
A896FBE02966B41300A3400B /* ViewModel */,
);
path = BottleMotion;
sourceTree = "<group>";
Expand All @@ -690,14 +687,6 @@
path = Home;
sourceTree = "<group>";
};
A896FBE02966B41300A3400B /* ViewModel */ = {
isa = PBXGroup;
children = (
A499318027BF3A38009FF5A8 /* BottleViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
};
A896FBE12966B41800A3400B /* UI */ = {
isa = PBXGroup;
children = (
Expand All @@ -710,7 +699,7 @@
A896FBE22966B41A00A3400B /* Controller */ = {
isa = PBXGroup;
children = (
A8BD833427BE106C00E0DE41 /* BottleViewController.swift */,
A8BD833427BE106C00E0DE41 /* BottleMotionViewController.swift */,
);
path = Controller;
sourceTree = "<group>";
Expand Down Expand Up @@ -1425,7 +1414,7 @@
A4C1AFCE27E4F8150096CD3E /* UIView+FadeInOut.swift in Sources */,
A49A8C25295331B000D5468A /* AssetImage.swift in Sources */,
A491018727D6F37C0012DFDD /* NSObject+Name.swift in Sources */,
A8BD833527BE106C00E0DE41 /* BottleViewController.swift in Sources */,
A8BD833527BE106C00E0DE41 /* BottleMotionViewController.swift in Sources */,
A4CF2C8027C733FE001B01B1 /* ColorButton.swift in Sources */,
A4CF2C8227C73B42001B01B1 /* UIColor+AssetColors.swift in Sources */,
A819CF9F27DDD97C00DE8E72 /* HapticManager.swift in Sources */,
Expand Down Expand Up @@ -1503,7 +1492,6 @@
A4F5715827DC459B00E7DF9B /* NoteColor.swift in Sources */,
A46B10FE281450F6004AB185 /* CustomNavigationController.swift in Sources */,
A8BD833327BE104900E0DE41 /* HomeViewController.swift in Sources */,
A499318127BF3A38009FF5A8 /* BottleViewModel.swift in Sources */,
A4396B1929323131005D9D3A /* ImageManager.swift in Sources */,
A8D58DB0299286A700FD52A1 /* NewBottleDateView.swift in Sources */,
A4D6EB77282E307900553E43 /* VersionManager.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// BottleViewController.swift
// BottleMotionViewController.swift
// Happiggy-bank
//
// Created by 권은빈 on 2022/02/16.
Expand All @@ -9,91 +9,81 @@ import UIKit

// TODO: 진행중인 유리병 있는지 없는지에 따라 초기 화면 구성 및 동작 수행 필요
/// 각 bottle 의 뷰를 관리하는 컨트롤러
final class BottleViewController: UIViewController {

// MARK: - @IBOutlet
final class BottleMotionViewController: UIViewController {

/// 저금통 쪽지를 보여주는 애니메이션 뷰
@IBOutlet weak var bottleNoteView: BottleNoteView!


// MARK: - Properties

/// BottleViewController 에 필요한 형태로 데이터를 가공해주는 View Model
var viewModel: BottleViewModel!

private var bottle: Bottle?

/// 애니메이션을 위한 중력 객체
var gravity: Gravity?
private var gravity: Gravity?

/// 쪽지를 넣기 위해 bottle note view 의 영역을 나눠줄 그리드
private lazy var grid: Grid = self.makeGrid()

/// 쪽지 노드
private var noteNodes: [NoteView] {
self.bottleNoteView.subviews.compactMap { $0 as? NoteView }
}
private var noteNodes: [NoteView] { self.view.subviews.compactMap { $0 as? NoteView } }

/// 쪽지가 최초로 다른 쪽지 혹은 바운더리와 부딪힐 때 한번만 모션 효과를 주기 위한 프로퍼티
private var activateHapticOnlyOnceForNewlyAddedNote = false

/// 새로운 쪽지가 추가될 때 상단 중앙에서 떨어지는 효과를 위해 지정할 프레임
private var topCenterFrame: CGRect {
let origin = CGPoint(
x: self.bottleNoteView.bounds.midX - self.grid.cellSize.width / 2,
x: self.view.bounds.midX - self.grid.cellSize.width / 2,
y: .zero
)

return CGRect(origin: origin, size: self.grid.cellSize)
}



// MARK: - Inits

// FIXME: - get rid of default value
init(bottle: Bottle?) {
self.bottle = bottle
super.init(nibName: nil, bundle: nil)
}


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


// MARK: - View Lifecycle

override func viewDidLoad() {
super.viewDidLoad()
self.addObservers()

override func viewLayoutMarginsDidChange() {
super.viewLayoutMarginsDidChange()

self.configureBottleMotionView()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

self.gravity?.enable()
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

self.gravity?.disable()
}

override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()

guard self.gravity == nil,
self.bottleNoteView != nil
else { return }
self.initializeBottleView()
}


// MARK: - @objc

/// 쪽지 새로 추가되었다는 알림을 받았을 때 호출되는 메서드
@objc private func noteDidAdd(_ notification: Notification) {

guard let noteAndDelay = notification.object as? (note: Note, delay: TimeInterval)
else { return }

self.dropNewNoteFromTopCenter(noteAndDelay.note, delay: noteAndDelay.delay)
}


// MARK: - Functions

/// 새로운 저금통이 추가되었을 때 호출되는 메서드
func bottleDidAdd(_ bottle: Bottle) {
self.viewModel.bottle = bottle
self.initializeBottleView()
self.bottle = bottle
self.configureBottleMotionView()
}

// FIXME: 홈탭 뷰컨 리팩토링 후 연결 메서드 전체적으로 고치기...쪽지 연동도 아직 미완
func noteDidAdd(_ note: Note, delay: TimeInterval) {
self.dropNewNoteFromTopCenter(note, delay: delay)
}

/// 현재 뷰 컨트롤러로 unwind 하라는 호출을 받았을 때 실행되는 액션메서드로, 중력 효과 재개
Expand Down Expand Up @@ -121,7 +111,7 @@ final class BottleViewController: UIViewController {
/// 중력 해제, 저금통 nil 처리, 쪽지 노드 제거
func bottleDidOpen(withDuration duration: TimeInterval) {
self.gravity = nil
self.viewModel.bottle = nil
self.bottle = nil
UIView.transition(
with: self.view,
duration: duration,
Expand All @@ -130,68 +120,75 @@ final class BottleViewController: UIViewController {
self.noteNodes.forEach { $0.removeFromSuperview() }
}
}

/// 저금통 유리병 애니메이션 초기 세팅: 쪽지 작성이 가능한 상태로 변경
private func initializeBottleView() {
guard let bottle = self.viewModel.bottle
else { return }



// MARK: - Configuration Functions

/// 저금통이 있는 경우 기간에 맞게 그리드를 생성해 쪽지 뷰를 추가하고 중력 효과 생성
private func configureBottleMotionView() {
guard let bottle
else {
return
}

self.noteNodes.forEach { $0.removeFromSuperview() }
self.grid = self.makeGrid()
self.fillBottleNoteView(fromNotes: bottle.notes)
self.addGravity()
self.gravity?.enable()
self.gravity = self.makeGravity().then { $0.enable() }
}

/// 저금통 기간에 따른 그리드 생성
private func makeGrid() -> Grid {
guard let bottle = self.viewModel.bottle
else { return Grid(frame: .zero, cellCount: .zero) }

self.bottleNoteView.frame = self.viewModel.gridFrame(forView: self.view)
return Grid(frame: self.bottleNoteView.bounds, cellCount: bottle.duration)
}

/// NotificationCenter.default 에 observer 들을 추가
private func addObservers() {
self.observe(
selector: #selector(noteDidAdd(_:)),
name: .noteDidAdd
guard let bottle
else {
return Grid(frame: .zero, cellCount: .zero)
}

let addtionalTopInset = bottle.duration < Metric.durationCap ? Metric.additionalTopInset : .zero
let gridFrame = self.view.bounds.inset(
by: .init(
top: Metric.topInset + addtionalTopInset,
left: Metric.horizontalInset,
bottom: Metric.bottomInset,
right: Metric.horizontalInset
)
)

return Grid(frame: gridFrame, cellCount: bottle.duration)
}

/// BottleNoteView 에 쪽지 이미지들을 추가
private func fillBottleNoteView(fromNotes notes: [Note]) {

for (index, note) in notes.enumerated() {
let frame = self.grid[index] ?? .zero
let noteView = self.createNoteView(note, frame: frame)
self.bottleNoteView.addSubview(noteView)
self.view.addSubview(noteView)
}
}

/// 그리드를 사용해서 bottleNoteView 의 해당 좌표에 들어갈 NoteView 생성
private func createNoteView(_ note: Note, frame: CGRect) -> NoteView {
NoteView(frame: frame, image: .note(color: note.color))
NoteView(frame: frame, image: AssetImage.note(ofColor: note.color) ?? UIImage())
}

/// 쪽지 이미지들에 중력 효과 추가
/// 유저가 폰을 기울이는 방향으로 쪽지들이 떨어짐
private func addGravity() {
gravity = Gravity(
private func makeGravity() -> Gravity {
Gravity(
dynamicItems: self.noteNodes,
referenceView: self.view,
collisionBoundaryInsets: Metric.collisionBoundaryInsets,
queue: nil
)

self.gravity?.collision.collisionDelegate = self
).then {
$0.collision.collisionDelegate = self
}
}

/// 새로운 쪽지를 추가하고 화면 상단 중앙에서 떨어트림
private func dropNewNoteFromTopCenter(_ note: Note, delay: TimeInterval) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
let noteView = self.createNoteView(note, frame: self.topCenterFrame)
self.bottleNoteView.addSubview(noteView)
self.view.addSubview(noteView)
self.gravity?.addDynamicItem(noteView)
self.activateHapticOnlyOnceForNewlyAddedNote.toggle()
}
Expand All @@ -200,7 +197,7 @@ final class BottleViewController: UIViewController {


// MARK: - UICollisionBehaviorDelegate
extension BottleViewController: UICollisionBehaviorDelegate {
extension BottleMotionViewController: UICollisionBehaviorDelegate {

// MARK: - Functions

Expand Down Expand Up @@ -239,3 +236,29 @@ extension BottleViewController: UICollisionBehaviorDelegate {
self.gravity?.startDeviceMotionUpdates()
}
}


// MARK: - Constants
fileprivate extension BottleMotionViewController {

/// BottleMotionViewController 에서 설정하는 layout 에 적용할 상수값들을 모아놓은 enum
enum Metric {

/// 현재 뷰를 기준으로 충돌 영역 설정을 위해 넣을 상하좌우 마진
static let collisionBoundaryInsets = UIEdgeInsets(top: .zero, left: 3, bottom: 3, right: 3)

/// 쪽지들이 바운더리와 부딪칠 때 햅틱 반응의 강도: 0.4
static let impactHapticIntensity: CGFloat = 0.4

/// 그리드 인셋
static let topInset: CGFloat = 30
static let bottomInset: CGFloat = 0
static let horizontalInset: CGFloat = 7

/// 1년 짜리 제외 다 영역 축소
static let durationCap = 300

/// 일주일, 한 달인 경우 높이를 축소하기 위해 감산해줄 값: 40
static let additionalTopInset: CGFloat = 40
}
}
Loading

0 comments on commit eb5ae49

Please sign in to comment.