Instantly share code, notes, and snippets.
Created
June 5, 2024 05:52
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save Kyle-Ye/2ad125fe173ed771e4769cab69c14616 to your computer and use it in GitHub Desktop.
A simple SwiftUI + UIHostingController sidebar implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SidebarController.swift | |
// | |
// | |
// Created by Kyle on 2024/6/3. | |
// | |
import SnapKit | |
import SwiftUI | |
import UIKit | |
public class SidebarController: UIViewController { | |
public static var shared = SidebarController() | |
public func setBackgroundColor(_ color: UIColor) { | |
self.color = color | |
} | |
private var color: UIColor? { | |
didSet { | |
sidebar.backgroundColor = color?.withAlphaComponent(0.8) | |
} | |
} | |
public func setContentView<Content: View>(_ content: () -> Content) { | |
let hostingController = UIHostingController(rootView: content()) | |
hostingController.view.backgroundColor = .clear | |
hostingController.view.translatesAutoresizingMaskIntoConstraints = false | |
self.hostingController = hostingController | |
} | |
private var hostingController: UIViewController? | |
public func show(in viewController: UIViewController) { | |
viewController.addChild(self) | |
viewController.view.addSubview(view) | |
didMove(toParent: viewController) | |
reset() | |
} | |
func reset() { | |
UIView.animate(withDuration: 0.3) { [self] in | |
dimmingView.alpha = 1 | |
sidebar.frame = sidebarShowFrame | |
} | |
} | |
private var isHiding = false | |
public func hide() { | |
guard !isHiding else { | |
return | |
} | |
isHiding = true | |
UIView.animate(withDuration: 0.3) { [self] in | |
dimmingView.alpha = 0 | |
sidebar.frame = sidebarHideFrame | |
} completion: { [self] _ in | |
isHiding = false | |
view.removeFromSuperview() | |
removeFromParent() | |
} | |
} | |
override public func viewDidLoad() { | |
// dimmingView config | |
view.addSubview(dimmingView) | |
dimmingView.frame = view.bounds | |
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDimmingViewTapped)) | |
dimmingView.addGestureRecognizer(tapGesture) | |
// sidebar config | |
view.addSubview(sidebar) | |
sidebar.frame = sidebarHideFrame | |
let blurView = IntensityVisualEffectView(effect: UIBlurEffect(style: .dark), intensity: 0.8) | |
blurView.frame = sidebar.bounds | |
blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight] | |
sidebar.addSubview(blurView) | |
sidebar.addSubview(sidebarMask) | |
sidebarMask.snp.makeConstraints { make in | |
make.edges.equalToSuperview() | |
} | |
// Pan gesture support | |
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:))) | |
sidebar.addGestureRecognizer(panGesture) | |
} | |
public override func didMove(toParent parent: UIViewController?) { | |
guard let hostingController else { | |
Log.runtimeIssues("You need to call SidebarController.shared.setContentView first") | |
return | |
} | |
if let parent { | |
addChild(hostingController) | |
sidebar.addSubview(hostingController.view) | |
hostingController.didMove(toParent: self) | |
NSLayoutConstraint.activate([ | |
hostingController.view.topAnchor.constraint(equalTo: sidebar.safeAreaLayoutGuide.topAnchor), | |
hostingController.view.leftAnchor.constraint(equalTo: sidebar.leftAnchor), | |
hostingController.view.rightAnchor.constraint(equalTo: sidebar.rightAnchor), | |
hostingController.view.bottomAnchor.constraint(equalTo: sidebar.bottomAnchor), | |
]) | |
} else { | |
hostingController.didMove(toParent: nil) | |
hostingController.view.removeFromSuperview() | |
hostingController.removeFromParent() | |
} | |
} | |
// MARK: - Init | |
private init() { | |
super.init(nibName: nil, bundle: nil) | |
} | |
@available(*, unavailable) | |
required init?(coder _: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
// MARK: - Private properties | |
private let dimmingView = { | |
let view = UIView(frame: .zero) | |
view.backgroundColor = .mask3 | |
return view | |
}() | |
private let sidebar = UIView(frame: .zero) | |
private let sidebarMask = { | |
let view = UIView(frame: .zero) | |
view.backgroundColor = .mask3 | |
return view | |
}() | |
// MARK: - Log related | |
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "KYSidebar") | |
// MARK: - ObjctiveC Selector | |
@objc | |
private func handleDimmingViewTapped() { | |
logger.debug("Sidebar dimmingView tap detected") | |
hide() | |
} | |
@objc | |
private func handleSwipe() { | |
logger.debug("Sidebar swipe detected") | |
hide() | |
} | |
private var sidebarWidth: CGFloat { | |
min(view.bounds.width * 0.84, 400) | |
} | |
private var sidebarHeight: CGFloat { | |
view.bounds.height | |
} | |
private var sidebarShowFrame: CGRect { | |
sidebarHideFrame.offsetBy(dx: -sidebar.frame.size.width, dy: 0) | |
} | |
private var sidebarHideFrame: CGRect { | |
CGRect(x: view.bounds.width, y: 0, width: sidebarWidth, height: sidebarHeight) | |
} | |
@objc | |
private func handlePanGesture(gesture: UIPanGestureRecognizer) { | |
let translationX = gesture.translation(in: sidebar).x | |
let velocityX = gesture.velocity(in: sidebar).x | |
logger.debug("Sidebar pan gesture detected \(gesture.state.rawValue) \(translationX) \(velocityX)") | |
guard !isHiding else { | |
return | |
} | |
let offset = max(translationX, 0) | |
if gesture.state == .began { | |
if velocityX > 1000 { | |
hide() | |
} | |
} else if gesture.state == .changed { | |
sidebar.frame = sidebarShowFrame.offsetBy(dx: offset, dy: 0) | |
} else if gesture.state == .ended || gesture.state == .failed || gesture.state == .cancelled { | |
if offset > 0.5 * sidebarWidth { | |
hide() | |
} else { | |
reset() | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment