오티스의개발일기

SwiftUI BottomSheet 커스텀 바텀시트 만드는 방법 😆 [코드 제공] 본문

IOS 개발/BottomSheet

SwiftUI BottomSheet 커스텀 바텀시트 만드는 방법 😆 [코드 제공]

안되면 될때까지.. 2024. 12. 18. 15:11
728x90

안녕하세요

오늘은 커스텀 바텀시트를 만들어보려고합니다.

 

제가 만드려고하는 바텀시트는 기본적으로 UIKit를 사용해서 컨트롤러를 제작한후

시트자체는 UIKit
사용하는 View는 SwiftUI에서 제작한 View를 사용할수있도록 만드려고 합니다.

 

지금부터 만들 특별한 기능은 아래와 같습니다.

 

  • 바텀시트 드레그기능
  • 바텀시트 자동 높이조절기능
  • 바텀시트 핸들노출유무, 색변경
  • 바텀시트 색 변경
  • 바텀시트 뒤 배경색 변경
  • 바텀시트 배경색클릭시 dissmiss 콜백기능

 

 

미리보기

 

 

전체 코드 예제 깃헙

https://github.com/1domybest/CustomBottomSheet_example

 

GitHub - 1domybest/CustomBottomSheet_example

Contribute to 1domybest/CustomBottomSheet_example development by creating an account on GitHub.

github.com

 

 

바텀시트 라이브러리 깃헙

https://github.com/1domybest/CustomBottomSheetLibrary

 

GitHub - 1domybest/CustomBottomSheetLibrary

Contribute to 1domybest/CustomBottomSheetLibrary development by creating an account on GitHub.

github.com

 

 

바텀시트 내부에서 사용한 키보드 매니저 라이브러리 깃헙

https://github.com/1domybest/KeyboardManagerLibrary

 

GitHub - 1domybest/KeyboardManagerLibrary

Contribute to 1domybest/KeyboardManagerLibrary development by creating an account on GitHub.

github.com

 

 

프로젝트 구조

 

 

 

이 프로젝트는 기본적으로 UIKit를 베이스로하고 

SwiftUI를 사용하는 구조입니다.

자세한 기본구조는  아래 깃헙을 참고해주세요

 

https://github.com/1domybest/UIKit-SwiftUI_Project

 

GitHub - 1domybest/UIKit-SwiftUI_Project: The project is based on Storyboards but uses SwiftUI.

The project is based on Storyboards but uses SwiftUI. - 1domybest/UIKit-SwiftUI_Project

github.com

 

 

CustomUIKitBottomSheetOption

 

외부에서 사용할 시트의 옵션을 구조체로 만든 파일입니다


  
import Foundation
import SwiftUI
import UIKit
struct CustomUIKitBottomSheetOption {
var pk: UUID
var someView: AnyView
var sheetMode: UIModalPresentationStyle = .custom
// 핸들 가능 모드 custom, automatic , popover, pageSheet, formSheet
// 핸들 불가능 모드 currentContext, fullScreen, overCurrentContext, overFullScreen,
var sheetHeight: CGFloat = UIScreen.main.bounds.height / 2
var dragAvailable: Bool = true
var availableOutTouchClose: Bool = true
var showHandler: Bool = true
var backgroundColor: Color = Color.black.opacity(0.5)
var handlerColor: Color = Color.gray4
var sheetColor: Color = Color.white0
var sheetShadowColor: Color = Color.gray
var minimumHeight:CGFloat = 100.getHeightScaledDiagonal()
var maximumHeight:CGFloat = UIScreen.main.bounds.height
var hasKeyboard: Bool = false
var availableHasHandle: Bool {
return showHandler && (sheetMode == .custom ||
sheetMode == .automatic ||
sheetMode == .popover ||
sheetMode == .pageSheet ||
sheetMode == .formSheet )
}
var onScrolling: ((Double) -> Void)? = { _ in }
var onEndTouchScrolling: ((Double, Bool) -> Void)? = { _,_ in }
var onDismiss: (() -> Void)? = {}
}

 

CustomUIKitBottomSheet

중간에서 바텀시트를 담고있는 UIViewController입니다.

 


  
import UIKit
import SwiftUI
class CustomUIKitBottomSheet: UIViewController {
var customUIKitBottomSheetOption: CustomUIKitBottomSheetOption?
var customModalPresentationController: CustomModalPresentationController?
private var keyboardHeightConstraint: NSLayoutConstraint?
private var initialTouchPoint: CGPoint?
var handlerView: UIView?
var topSafeAreaSize: CGFloat = .zero
var scrollView: UIScrollView?
var hostingController: UIHostingController<AnyView>?
var keyboardManager: KeyboardManager?
var keyboardHeight: CGFloat = .zero
var isKeyboardOpen: Bool = false
var lastSizeOfScrollViewContentHeight: CGFloat = .zero
var currentSizeOfScrollViewContentHeight: CGFloat = .zero
var dragGesture: UIPanGestureRecognizer?
var isScrolling: Bool = false
var isTop: Bool = true
var isBottom: Bool = false
var isFinishedDragging: Bool = false
var isOverScreen: Bool = false
init(bottomSheetModel: CustomUIKitBottomSheetOption) {
self.customUIKitBottomSheetOption = bottomSheetModel
// 핸들 가능 모드 custom, automatic , popover, pageSheet, formSheet
// 핸들 불가능 모드 currentContext, fullScreen, overCurrentContext, overFullScreen,
super.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = self.customUIKitBottomSheetOption?.sheetMode ?? .custom
if self.customUIKitBottomSheetOption?.sheetMode == .custom {
self.transitioningDelegate = self
}
if self.customUIKitBottomSheetOption?.hasKeyboard ?? false {
self.keyboardManager = KeyboardManager()
self.keyboardManager?.setCallback(callback: self)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("CustomUIKitBottomSheet deinit")
}
func unreference () {
self.customUIKitBottomSheetOption = nil
self.customModalPresentationController = nil
if dragGesture != nil {
view.removeGestureRecognizer(dragGesture!)
dragGesture = nil
}
self.scrollView = nil
self.hostingController = nil
self.handlerView = nil
if self.keyboardManager != nil {
self.keyboardManager?.removeCallback()
self.keyboardManager = nil
}
}
override func viewDidLoad() {
super.viewDidLoad()
setView()
if self.customUIKitBottomSheetOption?.dragAvailable ?? false && self.customUIKitBottomSheetOption?.sheetMode == .custom {
addGestureRecognizers()
}
if let rootViewController = UIApplication.shared.windows.first?.rootViewController {
let safeAreaInsets = rootViewController.view.safeAreaInsets
topSafeAreaSize = safeAreaInsets.top
}
}
func setView () {
if self.customUIKitBottomSheetOption?.sheetMode == .custom {
setSheetView()
setupScrollView()
} else {
setHostingView()
}
// setHostingView()
if self.customUIKitBottomSheetOption?.showHandler ?? false || self.customUIKitBottomSheetOption?.availableHasHandle ?? false {
setHandlerView()
}
}
func addGestureRecognizers() {
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
view.addGestureRecognizer(dragGesture)
self.dragGesture = dragGesture
}
func setupScrollView() {
let scrollView = CustomScrollView()
scrollView.backgroundColor = self.customUIKitBottomSheetOption?.sheetColor.getUIColor()
scrollView.isScrollEnabled = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.alwaysBounceVertical = true
scrollView.showsVerticalScrollIndicator = true
scrollView.delegate = self
view.addSubview(scrollView)
var topPadding = self.customUIKitBottomSheetOption?.showHandler ?? false ? 40.0.getHeightScaledDiagonal() : 28.0.getHeightScaledDiagonal()
if self.customUIKitBottomSheetOption?.sheetMode != .custom {
topPadding = 0
}
// 오토레이아웃 설정
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: topPadding),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor) // 가로 스크롤 방지
])
// SwiftUI View를 AnyView로 감싸서 사용
let swiftUIView = self.customUIKitBottomSheetOption?.someView
let hostingController = UIHostingController(rootView: AnyView(swiftUIView))
hostingController.view.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: self.customUIKitBottomSheetOption?.sheetHeight ?? .zero)
scrollView.addSubview(hostingController.view)
addChild(hostingController)
hostingController.didMove(toParent: self)
hostingController.view.backgroundColor = self.customUIKitBottomSheetOption?.sheetColor.getUIColor()
self.hostingController = hostingController
self.scrollView = scrollView
}
func setHostingView () {
// SwiftUI View를 AnyView로 감싸서 사용
let swiftUIView = self.customUIKitBottomSheetOption?.someView
let hostingController = UIHostingController(rootView: AnyView(swiftUIView))
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(hostingController.view)
var topPadding = self.customUIKitBottomSheetOption?.showHandler ?? false ? 40.0.getHeightScaledDiagonal() : 28.0.getHeightScaledDiagonal()
if self.customUIKitBottomSheetOption?.sheetMode != .custom {
topPadding = 0
}
// 오토레이아웃 설정
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor, constant: topPadding),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostingController.view.widthAnchor.constraint(equalTo: view.widthAnchor) // 가로 스크롤 방지
])
addChild(hostingController)
hostingController.didMove(toParent: self)
self.hostingController = hostingController
}
func setSheetView () {
if self.customUIKitBottomSheetOption?.sheetHeight == UIScreen.main.bounds.height { return }
view.backgroundColor = self.customUIKitBottomSheetOption?.sheetColor.getUIColor()
let radius: CGFloat = 20.0 // 원하는 radius 값으로 변경
let corners: UIRectCorner = [.topLeft, .topRight]
let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
self.view.layer.mask = maskLayer
}
func setHandlerView() {
let handlerView = UIView()
handlerView.backgroundColor = self.customUIKitBottomSheetOption?.handlerColor.getUIColor()
handlerView.layer.cornerRadius = 2
view.addSubview(handlerView)
handlerView.translatesAutoresizingMaskIntoConstraints = false
var topPadding = 8.0.getHeightScaledDiagonal()
if self.customUIKitBottomSheetOption?.sheetMode != .custom ?? .custom {
topPadding = 12.0.getHeightScaledDiagonal()
}
NSLayoutConstraint.activate([
handlerView.widthAnchor.constraint(equalToConstant: 36),
handlerView.heightAnchor.constraint(equalToConstant: 4),
handlerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
handlerView.topAnchor.constraint(equalTo: view.topAnchor, constant: topPadding)
])
self.handlerView = handlerView
}
func dismissPresent () {
self.dismiss(animated: true, completion: {
self.customUIKitBottomSheetOption?.onDismiss?()
self.unreference()
})
}
func dismissPresent (animated: Bool = true, completion: @escaping () -> Void) {
self.dismiss(animated: animated, completion: {
self.customUIKitBottomSheetOption?.onDismiss?()
self.unreference()
completion()
})
}
func updateSheetHeight(newHeight: CGFloat) {
self.currentSizeOfScrollViewContentHeight = newHeight
var newHeight = newHeight
// 이곳에서 max확인해야함
let topPadding = self.customUIKitBottomSheetOption?.showHandler ?? false ? 40.0.getHeightScaledDiagonal() : 28.0.getHeightScaledDiagonal()
newHeight += topPadding
var adjustedLength = min(max(newHeight, self.customUIKitBottomSheetOption?.minimumHeight ?? .zero), self.customUIKitBottomSheetOption?.maximumHeight ?? .zero)
var contentSize:CGFloat = adjustedLength
if newHeight > UIScreen.main.bounds.height - (keyboardHeight) {
// 넘어섰을때
adjustedLength = UIScreen.main.bounds.height
if keyboardHeight > 0 {
adjustedLength = UIScreen.main.bounds.height - (keyboardHeight)
}
contentSize = newHeight
self.isOverScreen = true
self.scrollView?.isScrollEnabled = true
} else {
self.isOverScreen = false
self.scrollView?.isScrollEnabled = false
}
DispatchQueue.main.async {
self.customModalPresentationController?.setSheetHeight(sheetHeight: adjustedLength)
self.scrollView?.contentSize = CGSize(width: self.view.frame.width, height: contentSize) // 테스트를 위한 고정된 크기
self.hostingController?.view.frame.size = CGSize(width: self.view.frame.width, height: contentSize)
}
}
}

 

 

CustomModalPresentationController

실제 바텀시트인 UIPresentationController 입니다


  
import Foundation
import UIKit
// Custom PresentationController 정의
class CustomModalPresentationController: UIPresentationController {
// 추가적인 파라미터
var bottomSheetModel: CustomUIKitBottomSheetOption?
var keyboardHeight: CGFloat = .zero
// 초기화 메서드
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, bottomSheetModel: CustomUIKitBottomSheetOption?) {
self.bottomSheetModel = bottomSheetModel
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
deinit {
print("CustomModalPresentationController deinit")
}
func unreference () {
self.bottomSheetModel = nil;
}
func setKeyboardHeight(keyboardHeight: CGFloat) {
self.keyboardHeight = keyboardHeight
}
// 바텀 시트 스타일을 위한 높이 설정
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView = containerView else { return .zero }
let height: CGFloat = self.bottomSheetModel?.sheetHeight ?? .zero
return CGRect(
x: 0,
y: containerView.bounds.height - (height + keyboardHeight),
width: containerView.bounds.width,
height: height
)
}
// 배경을 어둡게 만드는 처리
override func presentationTransitionWillBegin() {
guard let containerView = containerView else { return }
if self.bottomSheetModel?.backgroundColor == .clear { return }
if self.bottomSheetModel?.sheetMode != .custom { return }
let dimmingView = UIView(frame: containerView.bounds)
dimmingView.backgroundColor = self.bottomSheetModel?.backgroundColor.getUIColor()
dimmingView.alpha = 0 // 처음에는 투명하게 설정
// 탭 제스처 인식기 추가
if self.bottomSheetModel?.availableOutTouchClose ?? false {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dimmingViewTapped))
dimmingView.addGestureRecognizer(tapGesture)
}
containerView.insertSubview(dimmingView, at: 0)
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
dimmingView.alpha = 1.0
}, completion: nil)
}
@objc func dimmingViewTapped() {
// 탭 이벤트가 발생했을 때 수행할 동작 (예: 바텀 시트 닫기)
CustomBottomSheetSingleTone.shared.hide(pk: self.bottomSheetModel?.pk)
}
override func dismissalTransitionWillBegin() {
guard let dimmingView = containerView?.subviews.first else { return }
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
dimmingView.alpha = 0.0
}, completion: { _ in
dimmingView.removeFromSuperview()
})
}
func setSheetHeight(sheetHeight: CGFloat) {
// Update the height in the model
DispatchQueue.main.async {
self.bottomSheetModel?.sheetHeight = sheetHeight
// Notify that the layout should be updated
if let containerView = self.containerView {
UIView.animate(withDuration: 0.3) {
self.presentedView?.frame = self.frameOfPresentedViewInContainerView
containerView.setNeedsLayout()
containerView.layoutIfNeeded()
}
}
}
}
}

 

CustomBottomSheetSingleTone

실제로 SwiftUI에서 사용할수 있도록 만든 싱글톤형식의 인터페이스입니다

 


  
import Foundation
import UIKit
class CustomBottomSheetSingleTone {
/// 싱글톤
public static var shared = CustomBottomSheetSingleTone()
var bottomSheetList: [(CustomUIKitBottomSheet, UUID, UUID?)] = [] // (시트 옵션, 시트 Pk, 호출하는 viwePk)
var topViewController:UIViewController?
private var bottomsheetQueue: DispatchQueue? = DispatchQueue(label: "bottomsheet.queue")
func updateSheetHeight(pk: UUID, height:CGFloat) {
// 필터링을 위해 for-in 루프와 enumerated()를 사용하여 인덱스와 요소를 동시에 가져옴
for (index, (bottomSheetViewController, bottomSheetUUID, viewPk)) in bottomSheetList.enumerated().reversed() {
if bottomSheetUUID == pk {
bottomSheetViewController.updateSheetHeight(newHeight: height)
}
}
}
func findBottomSheet(pk: UUID?) -> CustomUIKitBottomSheet? {
guard let sheetPk = pk else { return nil }
for (index, (bottomSheetViewController, bottomSheetUUID, viewPk)) in self.bottomSheetList.enumerated().reversed() {
if bottomSheetUUID == sheetPk {
return bottomSheetViewController
}
}
return nil
}
func hide(pk: UUID?) {
bottomsheetQueue?.async {
guard let sheetPk = pk else { return }
DispatchQueue.main.async {
// 필터링을 위해 for-in 루프와 enumerated()를 사용하여 인덱스와 요소를 동시에 가져옴
for (index, (bottomSheetViewController, bottomSheetUUID, viewPk)) in self.bottomSheetList.enumerated().reversed() {
if bottomSheetUUID == sheetPk {
// 배열에서 해당 요소를 제거
if self.bottomSheetList.indices.contains(index) {
// Alert를 dismiss 처리
self.bottomSheetList[index].0.dismissPresent()
self.bottomSheetList.remove(at: index)
} else {
print("Index \(index) is out of bounds.")
}
}
}
print("바텀시트 \(self.bottomSheetList)")
// 배열이 비어 있으면 nil로 설정
if self.bottomSheetList.isEmpty {
self.bottomSheetList = []
self.topViewController = nil
}
}
}
}
func hide(sheetPk: UUID?, completion: @escaping () -> Void) {
bottomsheetQueue?.async {
guard let sheetPk = sheetPk else { return }
DispatchQueue.main.async {
for (index, (bottomSheetViewController, bottomSheetUUID, viewPk)) in self.bottomSheetList.enumerated().reversed() {
if bottomSheetUUID == sheetPk {
// Alert를 dismiss 처리
bottomSheetViewController.dismissPresent(completion: {
completion()
})
// 배열에서 해당 요소를 제거
if self.bottomSheetList.indices.contains(index) {
self.bottomSheetList.remove(at: index)
} else {
print("Index \(index) is out of bounds.")
}
}
}
if self.bottomSheetList.isEmpty {
self.bottomSheetList = []
self.topViewController = nil
}
print("바텀시트 \(self.bottomSheetList)")
}
}
}
func hideAll(animated: Bool = true) {
bottomsheetQueue?.async {
DispatchQueue.main.async {
for (index, (bottomSheetViewController, bottomSheetUUID, viewPk)) in self.bottomSheetList.enumerated().reversed() {
bottomSheetViewController.dismissPresent(animated: animated, completion: {
})
}
if self.bottomSheetList.isEmpty {
self.bottomSheetList = []
self.topViewController = nil
}
}
}
}
func hideAll(viewPk: UUID?, animated: Bool = true) {
print("방송종료 시트 호출됨")
bottomsheetQueue?.async {
guard let currentViewPk = viewPk else { return }
DispatchQueue.main.async {
for (index, (bottomSheetViewController, bottomSheetUUID, viewPk)) in self.bottomSheetList.enumerated().reversed() {
guard let viewPk = viewPk else { return }
if viewPk == currentViewPk {
bottomSheetViewController.dismissPresent(animated: animated, completion: {
if self.bottomSheetList.indices.contains(index) {
self.bottomSheetList.remove(at: index)
} else {
print("Index \(index) is out of bounds.")
}
})
}
}
if self.bottomSheetList.isEmpty {
self.bottomSheetList = []
self.topViewController = nil
}
}
}
}
func show(customUIKitBottomSheetOption: CustomUIKitBottomSheetOption, sheetPk: UUID, viewPk: UUID?) {
bottomsheetQueue?.async {
DispatchQueue.main.async {
let bottomSheetViewController = CustomUIKitBottomSheet(bottomSheetModel: customUIKitBottomSheetOption)
guard let topController = self.getTopViewController() else { return }
if self.bottomSheetList.isEmpty { self.topViewController = topController }
topController.present(bottomSheetViewController, animated: true)
self.bottomSheetList.append((bottomSheetViewController, sheetPk, viewPk))
}
}
}
private func getTopViewController() -> UIViewController? {
guard let rootViewController = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first?.windows
.filter({ $0.isKeyWindow }).first?.rootViewController else {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
_ = self.getTopViewController()
}
return nil
}
return getTopViewController(from: rootViewController)
}
private func getTopViewController(from rootViewController: UIViewController) -> UIViewController? {
if let presentedViewController = rootViewController.presentedViewController {
// rootViewController가 다른 뷰 컨트롤러를 표시 중이면, 그 뷰 컨트롤러를 최상단으로 확인
return getTopViewController(from: presentedViewController)
}
// Navigation Controller가 있는 경우
if let navigationController = rootViewController as? UINavigationController {
return navigationController.visibleViewController
}
// Tab Bar Controller가 있는 경우
if let tabBarController = rootViewController as? UITabBarController {
if let selectedViewController = tabBarController.selectedViewController {
return getTopViewController(from: selectedViewController)
}
}
return rootViewController
}
}

 

 

 

사용 방법

 

MainView


  
import Foundation
import SwiftUI
struct MainView: View {
@ObservedObject var vm: MainViewModel = MainViewModel()
var body: some View {
ZStack {
Color.black
Button(action: {
self.vm.openSwiftUIBottomSheet()
}, label: {
Text("바텀시트 열기")
.padding(.horizontal, 20)
.padding(.vertical, 15)
.foregroundColor(.white)
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.blue)
)
})
}
}
}

 

 

MainViewModel


  
import Foundation
import SwiftUI
import UIKit
class MainViewModel: ObservableObject {
var vmPK: UUID = UUID() // viewModel의 고유 pk
init() {
}
func openSwiftUIBottomSheet() {
let bottomSheetPk = UUID()
let view:AnyView = AnyView(
CustomBottomSheetView(pk: bottomSheetPk)
)
var bottomSheetOption = CustomUIKitBottomSheetOption(pk: bottomSheetPk, someView: view)
bottomSheetOption.sheetColor = .white
bottomSheetOption.handlerColor = .black
bottomSheetOption.dragAvailable = true
bottomSheetOption.hasKeyboard = true
bottomSheetOption.sheetHeight = 604.getHeightScaledDiagonal()
bottomSheetOption.sheetMode = .custom
self.openBottomSheet(bottomSheetOption: bottomSheetOption)
}
/// 바텀시트 열기
func openBottomSheet(bottomSheetOption: CustomUIKitBottomSheetOption) {
CustomBottomSheetSingleTone.shared.show(customUIKitBottomSheetOption: bottomSheetOption, sheetPk: bottomSheetOption.pk, viewPk: self.vmPK)
}
/// 열려있는 모든 바텀시트 닫기
func hideAllBottomSheet() {
CustomBottomSheetSingleTone.shared.hideAll(viewPk: self.vmPK, animated: false)
}
}

 

 

 

 

실제 바텀시트의 UI를 담당하는 파일

 

CustomBottomSheetView

 


  
import Foundation
import SwiftUI
import CustomBottomSheetLibrary
struct CustomBottomSheetView: View {
@ObservedObject var vm:CustomBottomSheetViewModel
init(pk: UUID) {
vm = CustomBottomSheetViewModel(pk: pk)
}
var body: some View {
ZStack {
VStack {
Text("Hello, this is a SwiftUI View inside a UIViewController!")
.font(.title)
.foregroundColor(.red)
.padding()
UIKitViewRepresentable(view: self.vm.textViewView)
.frame(height: self.vm.textViewHeight)
Button(action: {
vm.extraHeight += 100
print("높이 토글")
}) {
Text("시트 높이 늘리기")
.font(.headline)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
UIKitViewRepresentable(view: self.vm.textFieldView)
.frame(height: 50)
Button(action: {
// CustomBottomSheetSingleTone.shared.hide(pk: pk)
self.vm.keyboardManager?.hideKeyboard()
}) {
Text("닫기 버튼")
.font(.headline)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: {
self.vm.openSheet()
}) {
Text("추가 시트 버튼")
.font(.headline)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
RoundedRectangle(cornerRadius: 20)
.frame(width: 20, height: vm.extraHeight)
Button(action: {
CustomBottomSheetSingleTone.shared.hideAll()
}) {
Text("모든 시트 닫기")
.font(.headline)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
.background(
GeometryReader { geo -> Color in
DispatchQueue.main.async {
print("size: \(geo.size.height)")
if vm.hasChild { return }
if vm.lastHeight != geo.size.height {
self.vm.lastHeight = geo.size.height
CustomBottomSheetSingleTone.shared.updateSheetHeight(pk: self.vm.pk, height: geo.size.height)
}
}
return Color.clear
}
)
.ignoresSafeArea(.all)
.ignoresSafeArea(.keyboard)
}
}

 

 

CustomBottomSheetViewModel


  
//
// CustomBottomSheetViewModel.swift
// UIKitCustomBottomSheet
//
// Created by 온석태 on 12/17/24.
//
import Foundation
import SwiftUI
import CustomBottomSheetLibrary
import KeyboardManager
import CustomTextFieldLibrary
class CustomBottomSheetViewModel: ObservableObject {
var bottomSheetOption: CustomUIKitBottomSheetOption?
var navigationController: NavigationController? // 네비게이션
var customUIKitBottomSheet: CustomUIKitBottomSheet?
var keyboardManager: KeyboardManager?
var pk: UUID // 자신을 호출한 부모에서 생성한 자식의 고유 pk
var hasChild: Bool = false // 자기자신이 또다른 바텀시트가 있는지에대한 여부
/// 키보드관련 변수
var lastOffset: CGFloat = .zero // 마지막 스크롤 오프셋
var isUpScrolling: Bool = false // 위로 스크롤하는지에대한 여부
@Published var lastHeight: CGFloat = .zero // 마지막 바텀시트의 높이
@Published var extraHeight: CGFloat = .zero // 현재 바텀시트의 높이
/* 텍스트 필드 */
var textFieldView: SingleTextFieldContentView?
@Published var textFieldText:String = "" // 제목 텍스트
/* 텍스트 뷰 */
var textViewView: SingleTextViewContentView?
@Published var textViewHeight:CGFloat = 40 // 웰컴코멘트 동적 높이
@Published var textViewText:String = "" // 웰컴코멘트 텍스트
init(pk: UUID) {
self.pk = pk
keyboardManager = KeyboardManager()
keyboardManager?.setCallback(callback: self)
self.initializeField()
}
func initializeField () {
var textViewOption = SingleTextViewOption()
textViewOption.backgroundColor = .clear
textViewOption.textColor = .white
textViewOption.placeholder = "텍스트 뷰 플레이스 홀더"
textViewOption.backgroundColor = .brown
textViewOption.placeholderColor = .black
textViewOption.enableAutoHeight = true
textViewOption.minHeight = 40
textViewOption.maxHeight = 72
textViewOption.maximunLenght = 1000
textViewOption.font = UIFont.systemFont(ofSize: 14)
textViewOption.horizontalPadding = 14.getWidthScaledDiagonal()
textViewOption.verticalPadding = 11.getHeightScaledDiagonal()
textViewOption.onChangeHeight = {[ weak self ] height in
guard let self = self else { return }
self.textViewHeight = height
}
textViewOption.onTextChanged = {[ weak self ] text in
guard let self = self else { return }
self.textViewText = text
}
var textFieldOption = SingleTextFieldOption()
textFieldOption.font = UIFont.systemFont(ofSize: 14)
textFieldOption.textColor = .white
textFieldOption.backgroundColor = .blue
textFieldOption.placeholder = "텍스트 필드 플레이스 홀더"
textFieldOption.placeholderColor = .black
textFieldOption.leftPadding = 14.getWidthScaledDiagonal()
textFieldOption.placeholderColor = .white.withAlphaComponent(0.5)
textFieldOption.borderStyle = .none
textFieldOption.onTextChanged = { [ weak self ] text in
guard let self = self else { return }
self.textFieldText = text
}
self.textViewView = SingleTextViewContentView(singleTextViewOption: textViewOption)
self.textFieldView = SingleTextFieldContentView(singleTextFieldOption: textFieldOption)
}
func setNavigationController(navigationController: NavigationController) {
self.navigationController = navigationController
}
func openSheet() {
DispatchQueue.main.async {
let pk = UUID()
let bottomSheetModel = CustomUIKitBottomSheetOption(pk: pk, someView: AnyView(CustomBottomSheetView(pk: pk)))
self.bottomSheetOption = bottomSheetModel
self.bottomSheetOption?.backgroundColor = .blue.opacity(0.5)
self.bottomSheetOption?.availableOutTouchClose = true
self.bottomSheetOption?.sheetColor = .red
self.bottomSheetOption?.hasKeyboard = true
self.bottomSheetOption?.handlerColor = .black
self.bottomSheetOption?.dragAvailable = true
self.bottomSheetOption?.sheetMode = .custom
self.bottomSheetOption?.sheetHeight = 300
self.bottomSheetOption?.onDismiss = {
self.hasChild = false
}
if let bottomSheetModel = self.bottomSheetOption {
let pk = bottomSheetModel.pk
self.hasChild = true
CustomBottomSheetSingleTone.shared.show(customUIKitBottomSheetOption: bottomSheetModel, sheetPk: pk, viewPk: UUID())
}
}
}
func onViewAppear() {
DispatchQueue.main.async {
if self.customUIKitBottomSheet == nil {
self.customUIKitBottomSheet = CustomBottomSheetSingleTone.shared.findBottomSheet(pk: self.pk)
self.customUIKitBottomSheet?.customUIKitBottomSheetOption?.onScrolling = { [weak self] offset in
guard let self = self else { return }
if offset > self.lastOffset {
// 위로 스크롤 중
self.isUpScrolling = true
} else if offset < self.lastOffset {
// 아래로 스크롤 중
self.isUpScrolling = false
}
self.lastOffset = offset
}
self.customUIKitBottomSheet?.customUIKitBottomSheetOption?.onEndTouchScrolling = { [weak self] offset, decelerate in
guard let self = self else { return }
if !self.isUpScrolling {
if ((self.keyboardManager?.isKeyboardShown) != nil) {
DispatchQueue.main.async {
self.keyboardManager?.hideKeyboard()
}
}
}
}
}
}
}
func onViewDisAppear() {
// self.unreference()
}
}
extension CustomBottomSheetViewModel: KeyboardManangerProtocol {
func keyBoardWillShow(notification: NSNotification, keyboardHeight: CGFloat) {
print("키보드 열림")
print("키보드 의 높이 \(keyboardHeight)")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
self.customUIKitBottomSheet?.scrollToBottom(animated: true)
}
}
func keyBoardWillHide(notification: NSNotification) {
print("키보드 닫힘")
}
}

 

 

 

자세한 코드는 아래 깃헙을 참고해주세요!

 

전체 코드 예제 깃헙

https://github.com/1domybest/CustomBottomSheet_example

 

GitHub - 1domybest/CustomBottomSheet_example

Contribute to 1domybest/CustomBottomSheet_example development by creating an account on GitHub.

github.com

 

 

바텀시트 라이브러리 깃헙

https://github.com/1domybest/CustomBottomSheetLibrary

 

GitHub - 1domybest/CustomBottomSheetLibrary

Contribute to 1domybest/CustomBottomSheetLibrary development by creating an account on GitHub.

github.com

 

 

바텀시트 내부에서 사용한 키보드 매니저 라이브러리 깃헙

https://github.com/1domybest/KeyboardManagerLibrary

 

GitHub - 1domybest/KeyboardManagerLibrary

Contribute to 1domybest/KeyboardManagerLibrary development by creating an account on GitHub.

github.com

 

 

바텀시트 내부에서 사용한 키보드 텍스트 [필드, 뷰] 관련 깃헙

https://github.com/1domybest/CustomTextFieldLibrary

 

GitHub - 1domybest/CustomTextFieldLibrary

Contribute to 1domybest/CustomTextFieldLibrary development by creating an account on GitHub.

github.com

 

 

 

키보드 관련글

https://otis.tistory.com/entry/SwiftUI-%ED%82%A4%EB%B3%B4%EB%93%9C-%EB%A7%A4%EB%8B%88%EC%A0%80-%EB%A7%8C%EB%93%A4%EA%B8%B0%F0%9F%98%80-%EC%BD%94%EB%93%9C-%EC%A0%9C%EA%B3%B5

 

SwiftUI 키보드 매니저 만들기😀 [코드 제공]

안녕하세요!오늘은 키보드를 열고 닫고혹은 키보드의 상태를 알수있는 코드를 작성하려고합니다.!!!기능으로는현재 키보드가 열리는 상태와 닫히는 상태를 콜백을통해 정보를 response받고그 정

otis.tistory.com

 

 

텍스트 필드 관련 글

 

https://otis.tistory.com/entry/SwiftUI-%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%85%8D%EC%8A%A4%ED%8A%B8-%ED%95%84%EB%93%9C-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%B7%B0-%EB%A7%8C%EB%93%9C%EB%8A%94%EB%B0%A9%EB%B2%95-%F0%9F%A5%B0-%EC%BD%94%EB%93%9C%EC%A0%9C%EA%B3%B5-UIKit-Custom-TextField-UIKit-Custom-TextView

 

SwiftUI 에서 사용이 가능한 커스텀 텍스트 필드, 텍스트 뷰 만드는방법 🥰 [코드제공] [UIKit Custom T

안녕하세요 오늘은 SwiftUI에서 사용할수있는 텍스트 필드와 뷰에 관해 포스팅하려고합니다 ㅎㅎㅎ! 이 매니저를 만들게된 계기는제가 진행하고있는 프로젝트의 UI베이스는 SwiftUI이고미니멈 타

otis.tistory.com

 

 

 

 

728x90
Comments