Skip to content

[IDLE-000] 유저는 완료된 동작에 대한 정보를 스낵바로 제공받을 수 있다. #60

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

Merged
merged 8 commits into from
Sep 12, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "success_check.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// IdleSnackBar.swift
// DSKit
//
// Created by choijunios on 9/12/24.
//

import UIKit
import RxSwift
import RxCocoa
import Entity

public struct IdleSnackBarRO {
let titleText: String
let icon: UIImage?
let backgroundColor: UIColor?

public init(
titleText: String="완료되었습니다.",
icon: UIImage?=DSIcon.successCheck.image,
backgroundColor: UIColor?=DSColor.gray500.color
) {
self.titleText = titleText
self.icon = icon
self.backgroundColor = backgroundColor
}
}

public class IdleSnackBar: UIView {

private let titleLabel: IdleLabel = {
let label = IdleLabel(typography: .Subtitle4)
label.attrTextColor = DSColor.gray0.color
return label
}()

private let titleIcon: UIImageView = {
let imageView = UIImageView()
imageView.tintColor = DSColor.gray0.color
return imageView
}()

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

setAppearance()
setLayout()
}

public required init?(coder: NSCoder) { nil }

func setAppearance() {
self.backgroundColor = DSColor.gray500.color
self.layer.cornerRadius = 8
}

func setLayout() {

self.layoutMargins = .init(top: 12, left: 16, bottom: 12, right: 16)

let mainStack = HStack([
titleIcon, titleLabel, Spacer()
], spacing: 4, alignment: .center)

[
mainStack
].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
self.addSubview($0)
}

NSLayoutConstraint.activate([
mainStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor),
mainStack.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor),
mainStack.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor),
mainStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor),
])
}

public func applyRO(_ ro: IdleSnackBarRO) {
self.backgroundColor = ro.backgroundColor
titleLabel.textString = ro.titleText
titleIcon.image = ro.icon
}
}

@available(iOS 17.0, *)
#Preview("Preview", traits: .defaultLayout) {
let view = IdleSnackBar(
frame: .init(
origin: .init(x: 20, y: 300),
size: .init(width: 300, height: 48)
)
)

DispatchQueue.main.asyncAfter(deadline: .now()+3) {

UIView.animate(withDuration: 0.35) {
view.alpha = 0
}
}
return view
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

guard let windowScene = scene as? UIWindowScene else { return }

let vm = BaseViewModel()
let vc = ViewController()
vc.bind(viewModel: vm)

window = UIWindow(windowScene: windowScene)
window?.rootViewController = UIViewController()
window?.rootViewController = vc

window?.makeKeyAndVisible()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
//

import UIKit
import BaseFeature

class ViewController: UIViewController {
class ViewController: BaseViewController {

override func viewDidLoad() {

Expand All @@ -24,6 +25,15 @@ class ViewController: UIViewController {
initialLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
initialLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])

}

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

self.viewModel?
.snackBar
.onNext(.init(titleText: "테스트테스트테스트"))
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ open class BaseViewController: UIViewController {
private var loadingDimissionRequested: Bool = false
private var loadingVC: UIViewController?

open var snackBarBottomPadding: CGFloat = 0.0

/// disposeBag
public let disposeBag = DisposeBag()

Expand All @@ -29,6 +31,12 @@ open class BaseViewController: UIViewController {

self.viewModel = viewModel

// Life cycle
rx.viewDidAppear
.map({ _ in })
.bind(to: viewModel.viewDidAppear)
.disposed(by: disposeBag)

// Alert
viewModel
.alertDriver?
Expand All @@ -44,6 +52,14 @@ open class BaseViewController: UIViewController {
})
.disposed(by: disposeBag)

// Snack bar
viewModel
.snackBarDriver?
.drive(onNext: { [weak self] snackBarRO in
self?.showSnackBar(ro: snackBarRO)
})
.disposed(by: disposeBag)

// 로딩
viewModel
.showLoadingDriver?
Expand Down Expand Up @@ -106,6 +122,53 @@ public extension BaseViewController {
}
}

// MARK: Snack bar
extension BaseViewController {

func showSnackBar(ro: IdleSnackBarRO) {

let viewSize = self.view.bounds.size
let horizontalPadding: CGFloat = 20
let bottomPadding: CGFloat = 20 + snackBarBottomPadding

let snackBarHeight: CGFloat = 48
let snackBarWidth: CGFloat = viewSize.width - horizontalPadding*2

let snackBarXPos: CGFloat = horizontalPadding
let snackBarYPos: CGFloat = viewSize.height - bottomPadding - snackBarHeight

let snackBar = IdleSnackBar(
frame: .init(
origin: .init(x: snackBarXPos, y: snackBarYPos),
size: .init(width: snackBarWidth, height: snackBarHeight)
)
)

// ro적용
snackBar.applyRO(ro)

// 스낵바를 하단에 감춘다
snackBar.transform = .init(translationX: 0, y: snackBarHeight+horizontalPadding)
snackBar.alpha = 0.5

// 뷰계층에 추가
view.addSubview(snackBar)

let snackBarShowingDuration: CGFloat = 2

UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn) {
snackBar.transform = .identity
snackBar.alpha = 1.0
} completion: { _ in

UIView.animate(withDuration: 0.2, delay: snackBarShowingDuration, options: .curveEaseIn) {
snackBar.transform = .init(translationX: 0, y: snackBarHeight+horizontalPadding)
snackBar.alpha = 0.5
}
}
}
}

public extension BaseViewController {

func showDefaultLoadingScreen() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ open class BaseViewModel {
public let alertObject: PublishSubject<IdleAlertObject> = .init()
var alertObjectDriver: Driver<IdleAlertObject>?

// Snack bar
public let snackBar: PublishSubject<IdleSnackBarRO> = .init()
var snackBarDriver: Driver<IdleSnackBarRO>?

// MARK: SnackBarStack
private var snackBarStack: [IdleSnackBarRO] = []

let viewDidAppear: PublishSubject<Void> = .init()

// 로딩
public let showLoading: PublishSubject<Void> = .init()
public let dismissLoading: PublishSubject<Void> = .init()
Expand All @@ -35,12 +44,29 @@ open class BaseViewModel {

self.alertObjectDriver = alertObject
.asDriver(onErrorDriveWith: .never())

self.snackBarDriver = snackBar
.asDriver(onErrorDriveWith: .never())

self.showLoadingDriver = showLoading
.asDriver(onErrorDriveWith: .never())

self.dismissLoadingDriver = dismissLoading
.asDriver(onErrorDriveWith: .never())

// life cycle
viewDidAppear
.compactMap { [weak self] in
let bars = self?.snackBarStack
self?.snackBarStack = []
return bars
}
.flatMap { bars in
Observable.from(bars)
}
.debounce(.milliseconds(350), scheduler: MainScheduler.asyncInstance)
.bind(to: snackBar)
.disposed(by: disposeBag)
}

public func mapStartLoading<T>(_ target: Observable<T>) -> Observable<T> {
Expand All @@ -64,4 +90,8 @@ open class BaseViewModel {
return item
}
}

public func addSnackBar(ro: IdleSnackBarRO) {
self.snackBarStack.append(ro)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

import UIKit
import PresentationCore
import BaseFeature
import UseCaseInterface
import Entity
import DSKit

public class EditPostCoordinator: ChildCoordinator {

Expand Down Expand Up @@ -51,5 +53,20 @@ public class EditPostCoordinator: ChildCoordinator {
popViewController()
parent?.removeChildCoordinator(self)
}

func coordinatorDidFinishWithSnackBar(ro: IdleSnackBarRO) {
let belowIndex = navigationController.children.count-2

if belowIndex >= 0 {
let belowVC = navigationController.children[belowIndex]

if let baseVC = belowVC as? BaseViewController {

baseVC.viewModel?.addSnackBar(ro: ro)
}
}

coordinatorDidFinish()
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
//

import UIKit
import BaseFeature
import PresentationCore
import UseCaseInterface
import Entity
import DSKit

public class PostDetailForCenterCoordinator: ChildCoordinator {

Expand Down Expand Up @@ -64,6 +66,21 @@ public class PostDetailForCenterCoordinator: ChildCoordinator {
popViewController()
parent?.removeChildCoordinator(self)
}

public func coordinatorDidFinishWithSnackBar(ro: IdleSnackBarRO) {
let belowIndex = navigationController.children.count-2

if belowIndex >= 0 {
let belowVC = navigationController.children[belowIndex]

if let baseVC = belowVC as? BaseViewController {

baseVC.viewModel?.addSnackBar(ro: ro)
}
}

coordinatorDidFinish()
}
}

extension PostDetailForCenterCoordinator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ public class CenterRecruitmentPostBoardVM: BaseViewModel, CenterRecruitmentPostB

// 새로고침
closePostSuccess
.map({ [weak self] value in
// 스낵바
self?.snackBar.onNext(.init(titleText: "채용을 종료했어요."))

return value
})
.bind(to: requestOngoingPost)
.disposed(by: disposeBag)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,8 @@ public class EditPostVM: BaseViewModel, EditPostViewModelable {
guard let self else { return }

// 성공적으로 수정됨
self.editPostCoordinator?.coordinatorDidFinish()
self.editPostCoordinator?
.coordinatorDidFinishWithSnackBar(ro: .init(titleText: "공고가 수정되었어요."))
}
.disposed(by: disposeBag)

Expand Down
Loading