Skip to content

Instantly share code, notes, and snippets.

@Kyle-Ye
Created June 5, 2024 05:52
Show Gist options
  • Save Kyle-Ye/2ad125fe173ed771e4769cab69c14616 to your computer and use it in GitHub Desktop.
Save Kyle-Ye/2ad125fe173ed771e4769cab69c14616 to your computer and use it in GitHub Desktop.
A simple SwiftUI + UIHostingController sidebar implementation
//
// 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