Skip to content

Commit

Permalink
💄 :: [#812] Credit Song List Page UI
Browse files Browse the repository at this point in the history
  • Loading branch information
baekteun committed Jul 29, 2024
1 parent 83eb970 commit f524ab8
Show file tree
Hide file tree
Showing 29 changed files with 1,008 additions and 13 deletions.
1 change: 1 addition & 0 deletions Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ let targets: [Target] = [
.feature(target: .PlaylistFeature),
.feature(target: .MusicDetailFeature),
.feature(target: .SongCreditFeature),
.feature(target: .CreditSongListFeature),
.domain(target: .AppDomain),
.domain(target: .ArtistDomain),
.domain(target: .AuthDomain),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import CreditDomainTesting
@testable import CreditSongListFeature
import CreditSongListFeatureInterface
import Inject
import UIKit

@main
Expand All @@ -9,11 +13,55 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let viewController = UIViewController()
viewController.view.backgroundColor = .yellow
window?.rootViewController = viewController
let creditSongListTabFactory = FakeCreditSongListTabFactory()
let reactor = CreditSongListReactor(workerName: "CLTH")
let viewController = CreditSongListViewController(
reactor: reactor,
creditSongListTabFactory: creditSongListTabFactory
)
window?.rootViewController = Inject.ViewControllerHost(viewController)
window?.makeKeyAndVisible()

return true
}
}

final class FakeCreditSongListTabFactory: CreditSongListTabFactory {
func makeViewController(workerName: String) -> UIViewController {
let creditSongListTabItemFactory = FakeCreditSongListTabItemFactory()
return CreditSongListTabViewController(
workerName: workerName,
creditSongListTabItemFactory: creditSongListTabItemFactory
)
}
}

final class FakeCreditSongListTabItemFactory: CreditSongListTabItemFactory {
func makeViewController(workerName: String, sortType: CreditSongSortType) -> UIViewController {
let fetchCreditSongListUseCase = FetchCreditSongListUseCaseSpy()
fetchCreditSongListUseCase.handler = { _, _, _, _ in
.just([
.init(id: "6GQV6lhwgNs", title: "팬섭", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5qUwMpUWNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5qUwMpUWNQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5qUwMpUWLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5qUwMpUNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5qUwMpWNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5qUwMUWNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5qUwpUWNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5qUMpUWNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5qwMpUWNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "5UwMpUWNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "qUwMpUWNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024"),
.init(id: "UwMpUWNLQ", title: "신시대", artist: "세구", views: 100_000_000, date: "2024")
])
}

let reactor = CreditSongListTabItemReactor(
workerName: workerName,
creditSortType: sortType,
fetchCreditSongListUseCase: fetchCreditSongListUseCase
)
return Inject.ViewControllerHost(CreditSongListTabItemViewController(reactor: reactor))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import UIKit

public protocol CreditSongListFactory {
func makeViewController(workerName: String) -> UIViewController
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import UIKit

public protocol CreditSongListTabFactory {
func makeViewController(workerName: String) -> UIViewController
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import UIKit

public protocol CreditSongListTabItemFactory {
func makeViewController(workerName: String, sortType: CreditSongSortType) -> UIViewController
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
public enum CreditSongSortType: CaseIterable {
case latest
case popular
case oldest

public var display: String {
switch self {
case .latest:
return "최신순"
case .popular:
return "인기순"
case .oldest:
return "과거순"
}
}
}

This file was deleted.

7 changes: 5 additions & 2 deletions Projects/Features/CreditSongListFeature/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ let project = Project.module(
targets: [
.interface(module: .feature(.CreditSongListFeature)),
.implements(module: .feature(.CreditSongListFeature), dependencies: [
.feature(target: .CreditSongListFeature, type: .interface)
.feature(target: .BaseFeature),
.feature(target: .CreditSongListFeature, type: .interface),
.domain(target: .CreditDomain, type: .interface)
]),
.testing(module: .feature(.CreditSongListFeature), dependencies: [
.feature(target: .CreditSongListFeature, type: .interface)
Expand All @@ -16,7 +18,8 @@ let project = Project.module(
.feature(target: .CreditSongListFeature)
]),
.demo(module: .feature(.CreditSongListFeature), dependencies: [
.feature(target: .CreditSongListFeature)
.feature(target: .CreditSongListFeature),
.domain(target: .CreditDomain, type: .testing)
])
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import CreditSongListFeatureInterface
import NeedleFoundation
import UIKit

public protocol CreditSongListDependency: Dependency {
var creditSongListTabFactory: any CreditSongListTabFactory { get }
}

public final class CreditSongListComponent: Component<CreditSongListDependency>, CreditSongListFactory {
public func makeViewController(workerName: String) -> UIViewController {
let reactor = CreditSongListReactor(workerName: workerName)
let viewController = CreditSongListViewController(
reactor: reactor,
creditSongListTabFactory: dependency.creditSongListTabFactory
)
return viewController
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ReactorKit

final class CreditSongListReactor: Reactor {
enum Action {}
enum Mutation {}
struct State {
var workerName: String
}

let initialState: State
internal let workerName: String

init(workerName: String) {
self.initialState = .init(
workerName: workerName
)
self.workerName = workerName
}

func mutate(action: Action) -> Observable<Mutation> {
.empty()
}

func reduce(state: State, mutation: Mutation) -> State {
state
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import BaseFeature
import CreditSongListFeatureInterface
import DesignSystem
import RxSwift
import SnapKit
import UIKit
import Utility

final class CreditSongListViewController: BaseReactorViewController<CreditSongListReactor> {
private let creditProfileGradientContainerView = UIView().then {
$0.alpha = 0.6
}

private let wmNavigationBar = WMNavigationBarView()
private let dismissButton = UIButton(type: .system).then {
$0.setImage(DesignSystemAsset.Navigation.back.image, for: .normal)
$0.tintColor = .black
}

private let creditProfileView = CreditProfileView()
private let creditSongListTabViewController: UIViewController
private var creditSongListTabView: UIView { creditSongListTabViewController.view }

private var creditProfileViewTopConstraint: NSLayoutConstraint?
private var minusCreditProfileViewMaxHeight: CGFloat = 0
private var scrollHistory: [Int: CGFloat] = [:]

private var profileGradientLayer: CAGradientLayer?

init(
reactor: Reactor,
creditSongListTabFactory: any CreditSongListTabFactory
) {
self.creditSongListTabViewController = creditSongListTabFactory.makeViewController(
workerName: reactor.workerName
)
super.init(reactor: reactor)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if profileGradientLayer == nil {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [
DesignSystemAsset.BlueGrayColor.blueGray900.color.cgColor,
DesignSystemAsset.BlueGrayColor.blueGray900.color.withAlphaComponent(0.0).cgColor
]
profileGradientLayer?.frame = creditProfileGradientContainerView.bounds
creditProfileGradientContainerView.layer.addSublayer(gradientLayer)
profileGradientLayer = gradientLayer
}

if minusCreditProfileViewMaxHeight == 0, creditProfileView.frame.height != 0 {
minusCreditProfileViewMaxHeight = -creditProfileView.frame.height
}
}

override func addView() {
view.addSubviews(creditProfileGradientContainerView, wmNavigationBar, creditProfileView)
wmNavigationBar.setLeftViews([dismissButton])

addChild(creditSongListTabViewController)
view.addSubviews(creditSongListTabView)
creditSongListTabViewController.didMove(toParent: self)
}

override func setLayout() {
creditProfileGradientContainerView.snp.makeConstraints {
$0.top.horizontalEdges.equalToSuperview()
$0.height.equalTo(200)
}

wmNavigationBar.snp.makeConstraints {
$0.top.equalToSuperview().offset(STATUS_BAR_HEGHIT())
$0.horizontalEdges.equalToSuperview()
$0.height.equalTo(48)
}

creditProfileViewTopConstraint = creditProfileView.topAnchor.constraint(
equalTo: wmNavigationBar.bottomAnchor,
constant: 8
)
creditProfileViewTopConstraint?.isActive = true

creditProfileView.snp.makeConstraints {
$0.horizontalEdges.equalToSuperview().inset(20)
}

creditSongListTabView.snp.makeConstraints {
$0.top.equalTo(creditProfileView.snp.bottom).offset(24)
$0.horizontalEdges.bottom.equalToSuperview()
}
}

override func configureUI() {
view.backgroundColor = DesignSystemAsset.BlueGrayColor.blueGray100.color
}

override func configureNavigation() {}

override func bindAction(reactor: CreditSongListReactor) {
dismissButton.rx.tap
.bind(with: self) { owner, _ in
owner.navigationController?.popViewController(animated: true)
}
.disposed(by: disposeBag)
}

override func bindState(reactor: CreditSongListReactor) {
let sharedState = reactor.state.share()

sharedState.map(\.workerName)
.bind(with: self) { owner, name in
owner.creditProfileView.updateProfile(name: name)
}
.disposed(by: disposeBag)

CreditSongListScopedState.shared.creditSongTabItemScrolledObservable
.observe(on: MainScheduler.asyncInstance)
.bind(with: self) { owner, scrollView in
owner.songListDidScroll(scrollView: scrollView)
}
.disposed(by: disposeBag)
}
}

private extension CreditSongListViewController {
func songListDidScroll(scrollView: UIScrollView) {
guard let creditProfileViewTopConstraint else { return }

let scrollDiff = scrollView.contentOffset.y - scrollHistory[
scrollView.hash,
default: scrollView.contentOffset.y
]
let absoluteTop: CGFloat = 0
let absoluteBottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height
let isScrollingDown = scrollDiff > 0 && scrollView.contentOffset.y > absoluteTop
let isScrollingUp = scrollDiff < 0 && scrollView.contentOffset.y < absoluteBottom

if scrollView.contentOffset.y < absoluteBottom {
var newHeight = creditProfileViewTopConstraint.constant

if isScrollingDown {
newHeight = max(
minusCreditProfileViewMaxHeight,
creditProfileViewTopConstraint.constant - abs(scrollDiff)
)
} else if isScrollingUp {
if scrollView.contentOffset.y <= abs(minusCreditProfileViewMaxHeight) {
newHeight = min(
8,
creditProfileViewTopConstraint.constant + abs(scrollDiff)
)
}
}

if newHeight != creditProfileViewTopConstraint.constant {
creditProfileViewTopConstraint.constant = newHeight

let openAmount = creditProfileViewTopConstraint.constant + abs(minusCreditProfileViewMaxHeight)
let percentage = openAmount / abs(minusCreditProfileViewMaxHeight)
creditProfileView.alpha = percentage

self.view.layoutIfNeeded()
}

scrollHistory[scrollView.hash] = scrollView.contentOffset.y
}
}
}
Loading

0 comments on commit f524ab8

Please sign in to comment.