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

APP-10298 : PanModalNavigationController 추가 #10

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
133 changes: 133 additions & 0 deletions PanModal/Controller/PanModalNavigationController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import UIKit

public class PanModalNavigationController: UINavigationController, PanModalPresentable {
public var panScrollable: UIScrollView? {
return mTopPanModalPresentable?.panScrollable
}
public var longFormHeight: PanModalHeight {
return mTopPanModalPresentable?.longFormHeight ?? .maxHeight
}
public var shortFormHeight: PanModalHeight {
return mTopPanModalPresentable?.shortFormHeight ?? longFormHeight
}
public var panCustomTopView: PanCustomTopView? {
return mTopPanModalPresentable?.panCustomTopView
}
public var allowsTapToDismiss: Bool {
return mTopPanModalPresentable?.allowsTapToDismiss ?? true
}
public var allowsDragToDismiss: Bool {
return mTopPanModalPresentable?.allowsDragToDismiss ?? true
}
public var indicatorColor: UIColor {
return mTopPanModalPresentable?.indicatorColor ?? UIColor(red: 241/255.0, green: 243/255.0, blue: 245/255.0, alpha: 1)
}
public var panModalBackgroundColor: UIColor {
UIColor.black.withAlphaComponent(0.7)
}

public var dragIndicatorBackgroundColor: UIColor { .Palette.surface }

public var allowsExtendedPanScrolling = true
// 16: IndicatorView Size, 56: IndicatorView를 제외한 상단 마진
// IndicatorView는 TopOffset에 포함되어 있지 않음
// 만약 IndicatorView를 상단에 붙이고 싶으면 topOffset값은 16이 되어야 한다.
public var topOffset = CGFloat(56 + 16)

private var mIsInteractiveBack = false
private var mPushedControllerBeforeViewLoaded = [UIViewController]()
private var mLastStateStore = [Int: PanModalPresentationState]()
private var mTopPanModalPresentable: PanModalPresentable? {
return topViewController as? PanModalPresentable
}
private var borderCoordinator: BorderCoordinator?

public override func viewDidLoad() {
super.viewDidLoad()

delegate = self
interactivePopGestureRecognizer?.delegate = self
view.backgroundColor = .Palette.surface
navigationBar.isTranslucent = false
navigationBar.backgroundColor = .Palette.surface
navigationBar.hideNavigationBarBorder()

mPushedControllerBeforeViewLoaded.forEach { pushViewController($0, animated: false) }
mPushedControllerBeforeViewLoaded.removeAll()
borderCoordinator = BorderCoordinator(borderCoordinatorAdaptable: self)
}

public override func popViewController(animated: Bool) -> UIViewController? {
let vc = super.popViewController(animated: animated)

mIsInteractiveBack = vc?.transitionCoordinator?.isInteractive ?? false
if !mIsInteractiveBack {
panModalSetNeedsLayoutUpdate()
}
return vc
}

public override func pushViewController(_ viewController: UIViewController, animated: Bool) {
if !isViewLoaded {
mPushedControllerBeforeViewLoaded.append(viewController)
} else {
super.pushViewController(viewController, animated: animated)
panModalSetNeedsLayoutUpdate()
}
}

public func willTransition(to state: PanModalPresentationState) {
if let topControllerHashValue = topViewController?.hashValue {
mLastStateStore[topControllerHashValue] = state
}
}

public func shouldRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool {
return mTopPanModalPresentable?.shouldRespond(to: panModalGestureRecognizer) ?? true
}

func panModalSetNeedsLayoutUpdate() {
presentedVC?.setNeedsLayoutUpdate()
adjustPanModalState()
}

fileprivate func adjustPanModalState() {
panModalTransition(to: mLastStateStore[(topViewController ?? self).hashValue] ?? .shortForm)
}

public func willRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) {
if let presentable = topViewController as? PanModalPresentable {
presentable.willRespond(to: panModalGestureRecognizer)
}
}

public func panModalWillDismiss() {
if let presentable = topViewController as? PanModalPresentable {
presentable.panModalWillDismiss()
}
}
}

extension PanModalNavigationController: UINavigationControllerDelegate {
public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
if mIsInteractiveBack {
panModalSetNeedsLayoutUpdate()
}
}
}

extension PanModalNavigationController: UIGestureRecognizerDelegate {
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}

extension PanModalNavigationController: BorderCoordinatorAdaptable {
public var scrollView: UIScrollView {
return panScrollable ?? UIScrollView()
}

public var targetView: UIView {
return navigationBar
}
}
22 changes: 18 additions & 4 deletions PanModal/Presentable/PanModalPresentable+Defaults.swift
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PanModalNavigationController를 PanModal 안으로 옮기게된 이유입니다.

  1. extension으로 구현한 PanModalPresentable 디폴트 값이 PanModal 라이브러리 내부와 ZigZag내부에 둘다 정의되어있음
    • ZigZag에서 사용할 경우에는 ZigZag내부에 정의된 디폴트 값으로 덮어씌워지므로 상관 없음
    • 피처모듈에서 사용할 경우 두 디폴트값중 어느 값을 사용해야할지 알 수 없다는 오류가 발생
  2. PanModal을 사용한 네비게이션 뷰컨이므로 PanModal 내부에 있는것이 더 맞다는 생각이 듦

Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@ public extension PanModalPresentable where Self: UIViewController {
}

var longFormHeight: PanModalHeight {
guard let scrollView = panScrollable else { return .maxHeight }

guard let scrollView = panScrollable
else { return .maxHeight }
let navigationBarHeight = (navigationController?.navigationBar ?? getCustomNavigationBar())?.frame.height ?? 0

// called once during presentation and stored
scrollView.layoutIfNeeded()
return .contentHeight(scrollView.contentSize.height)
return .contentHeight(scrollView.contentSize.height + navigationBarHeight)
}

var springDamping: CGFloat {
Expand Down Expand Up @@ -117,4 +116,19 @@ public extension PanModalPresentable where Self: UIViewController {
func panModalDidDismiss() {

}

var parentPresentable: PanModalNavigationController? {
return navigationController as? PanModalNavigationController
}
}

private extension PanModalPresentable where Self: UIViewController {
func getCustomNavigationBar() -> UINavigationBar? {
for subview in self.view.subviews {
if let navigationBar = subview as? UINavigationBar {
return navigationBar
}
}
return nil
}
}
12 changes: 10 additions & 2 deletions PanModal/Presentable/PanModalPresentable+UIViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ public extension PanModalPresentable where Self: UIViewController {
function in the PanModalPresentationController.
*/
func panModalTransition(to state: PanModalPresentationState) {
presentedVC?.transition(to: state)
if let parentPresentable = parentPresentable {
parentPresentable.panModalTransition(to: state)
} else {
presentedVC?.transition(to: state)
}
}

/**
Expand All @@ -46,7 +50,11 @@ public extension PanModalPresentable where Self: UIViewController {
- Note: This should be called whenever any of the values for the PanModalPresentable protocol are changed.
*/
func panModalSetNeedsLayoutUpdate() {
presentedVC?.setNeedsLayoutUpdate()
if let parentPresentable = parentPresentable {
parentPresentable.panModalSetNeedsLayoutUpdate()
} else {
presentedVC?.setNeedsLayoutUpdate()
}
}

/**
Expand Down
27 changes: 27 additions & 0 deletions PanModal/UIColor+.swift
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PanModal에서 사용하는 디자인 시스템 컬러값들이 있어서 별도로 정의해주었습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컬러값을 외부에서 주입받을 수 있도록 변경

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import UIKit

extension UIColor {
enum Palette {
static var surface: UIColor {
return UIColor { traitCollection in
switch traitCollection.userInterfaceStyle {
case .dark:
return UIColor(red: 25/255, green: 25/255, blue: 25/255, alpha: 1.0)
default:
return UIColor.white
}
}
}

static var gray100: UIColor {
return UIColor { traitCollection in
switch traitCollection.userInterfaceStyle {
case .dark:
return UIColor(red: 33/255, green: 36/255, blue: 37/255, alpha: 1.0)
default:
return UIColor(red: 245/255, green: 246/255, blue: 248/255, alpha: 1.0)
}
}
}
}
}
105 changes: 105 additions & 0 deletions PanModal/View/BorderCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import UIKit
import Combine

class BorderCoordinator {
private var cancellables = Set<AnyCancellable>()
private var borderView: UIView?
private weak var borderCoordinatorAdaptable: BorderCoordinatorAdaptable?

init(borderCoordinatorAdaptable: BorderCoordinatorAdaptable) {
self.borderCoordinatorAdaptable = borderCoordinatorAdaptable

let scrollView = borderCoordinatorAdaptable.scrollView
let yOffset = borderCoordinatorAdaptable.yOffset

scrollView.publisher(for: \.contentOffset)
.removeDuplicates()
.map { $0.y >= yOffset }
.removeDuplicates()
.sink { [weak self] isShowingBorder in
if isShowingBorder {
self?.showBorder()
} else {
self?.hideBorder()
}
}
.store(in: &cancellables)
}

private func showBorder() {
if let navigationBar = borderCoordinatorAdaptable?.targetView as? UINavigationBar {
navigationBar.showNavigationBarBorder()
} else {
appendBorder()
}
}

private func hideBorder() {
if let navigationBar = borderCoordinatorAdaptable?.targetView as? UINavigationBar {
navigationBar.hideNavigationBarBorder()
} else {
borderView?.removeFromSuperview()
}
}

private func appendBorder() {
if borderView == nil {
borderView = UIView()
borderView?.backgroundColor = .Palette.gray100
borderView?.translatesAutoresizingMaskIntoConstraints = false
}

guard let borderView = borderView,
let targetView = borderCoordinatorAdaptable?.targetView else { return }

borderView.removeFromSuperview()
targetView.addSubview(borderView)
targetView.sendSubviewToBack(borderView)
borderView.heightAnchor.constraint(equalToConstant: 1).isActive = true
borderView.leadingAnchor.constraint(equalTo: targetView.leadingAnchor).isActive = true
borderView.trailingAnchor.constraint(equalTo: targetView.trailingAnchor).isActive = true
borderView.bottomAnchor.constraint(equalTo: targetView.bottomAnchor).isActive = true
}
}

protocol BorderCoordinatorAdaptable: AnyObject {
var scrollView: UIScrollView { get }
var yOffset: CGFloat { get }
var targetView: UIView { get }
}

extension BorderCoordinatorAdaptable {
var yOffset: CGFloat {
return 1
}
}

extension UINavigationBar {
func hideNavigationBarBorder() {
if #available(iOS 15.0, *) {
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
appearance.shadowColor = .clear
appearance.backgroundColor = barTintColor
standardAppearance = appearance
scrollEdgeAppearance = appearance
} else {
standardAppearance.shadowColor = .clear
standardAppearance.backgroundColor = barTintColor
}
}

func showNavigationBarBorder() {
if #available(iOS 15.0, *) {
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
appearance.shadowColor = .Palette.gray100
appearance.backgroundColor = barTintColor
standardAppearance = appearance
scrollEdgeAppearance = appearance
} else {
standardAppearance.shadowColor = .Palette.gray100
standardAppearance.backgroundColor = barTintColor
}
}
}