DADAHAE's Log
비싼 장난감 가지고 노는 중 (❁´▽`❁)*✲゚*
[SwiftUI] 네? iOS15에는 sheet의 크기를 조절할 수 없다구요? 그럼 직접 만들지 뭐...
2023.04.20 목요일
GitSpace 개발 중 차단/신고 모달 뷰를 half sheet로 구현하기 위해 작성한 글입니다.

 

iOS 16에는 pretent라고 해서 modal의 크기를 마음대로 조정할 수 있는 수정자를 제공한다.

그러면 그 이전 버전은요..?

-> '직접' 만들어써야 한다.

 

GitSpace는 iOS 15를 채택하고 있기 때문에, UIKit의 기능을 불러와서 직접 Half Modal을 구현하기로 했다.

 

아래는 구현에 필요한 파일들이다.

  • ContentView: View
  • extension View
  • HalfSheetManager << UIKit Integration
  • CustomHostingController

 

일반 수정자처럼 Half Modal을 사용할 수 있게 하자.

.sheet { ... }처럼 Half Modal도 .halfSheet { ... }처럼 사용해보자.

// Custom Half Modal Modifier
extension View {
    // Binding Show Vairables
    func halfSheet<SheetView: View> (
        showSheet: Binding<Bool>,
        @ViewBuilder sheetView: @escaping ()->SheetView
    ) -> some View {
        return self
    }
}

// ContentView
struct ContentView: View {
    @State var isShowing: Bool = false

    var body: some View {
        VStack { 
            Text("Hello!")
            Button {
                isShowing.toggle()
            } label: {
                Text("Show Half Modal Sheet.")
            }
        }
        .halfSheet {
            Text("This is Half Sheet.")
        }
    }
}

 

UIKit의 기능을 가져오자.

// UIKit Integration
struct HalfSheetManager<SheetView: View>: UIViewControllerRepresentable {
    var sheetView: SheetView
    let controller: UIViewController()

    func makeUIViewController(context: Context) -> UIViewController {
        controller.view.background = .clear
        return controller
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {

    }
}

 

HalfSheetModifier 수정

Swiftui frame 사이즈에 맞게 자동으로 맞춰지도록 코드를 수정하자.

// Custom Half Modal Modifier
extension View {
    // Binding Show Vairables
    func halfSheet<SheetView: View> (
        showSheet: Binding<Bool>,
        @ViewBuilder sheetView: @escaping ()->SheetView
    ) -> some View {

        // why we using overlay or background
        // because it will automatically use the swiftui frame size only!
        return self
            .background {
                HalfSheetManager(sheetView: sheetView(), showSheet: showSheet)
            }
    }
}

 

UIKit의 기능을 가져오자.

  • isShowing 프로퍼티 바인딩
    sheet가 나오는 상황을 조절해주는 isShowing을 바인딩하여 HalfSheetManager에 전달해주자.
// UIKit Integration
struct HalfSheetManager<SheetView: View>: UIViewControllerRepresentable {
    var sheetView: SheetView
    let controller: UIViewController()
    @Binding var showSheet: Bool

    func makeUIViewController(context: Context) -> UIViewController {
        controller.view.background = .clear
        return controller
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        if showSheet {
            // Presenting Modal View
            let sheetController = UIHostingController(rootView: sheetView)
            uiViewController.present(sheetController, animated = true) {
                // tooling the show State
                DispatchQueue.main.async {
                    self.showSheet.toggle()
                }
            }
        }
    }
}

 

Custom UIHostingController

UISheetPresentationController로 모달의 크기를 정해준다.
해당 클래스를 사용하여 모달의 크기를 설정해주자. 도큐먼트를 읽고 원하는데로 조정하면 된다!

// Custom UIHostingController for halfsheet
class CustomHostingController<Content: View>: UIHostingContrller<Content> {

    override func viewDidLoad() {
        // setting presentation controller properties
        if let presentationController = presentationController as? UISheetPresentationController {
            presentationController.detents = [
                .medium(),
                .large()
            ]

            // to show grab protion
            presentationController.preferGrabberVisible = true
        }
    }

}

 

dismiss 설정하기 + 전체 코드

/* VIEW */

struct ContentView: View {
    @State var showSheet: Bool = false

    var body: some View {
        VStack { 
            Text("Hello!")
            Button {
                showSheet.toggle()
            } label: {
                Text("Show Half Modal Sheet.")
            }
        }
        .halfSheet(showSheet: $showSheet) {
            VStack {
                Text("This is Half Sheet.")
                Button {
                    showSheet.toggle()
                } label: {
                    Text("Dismiss")
                }
            }
        } onEnd: {
            print("Dismissed.")
        }
    }
}

// Custom Half Modal Modifier
extension View {
    // Binding Show Vairables
    func halfSheet<SheetView: View> (
        showSheet: Binding<Bool>,
        @ViewBuilder sheetView: @escaping ()->SheetView,
        /* 새로 추가한 부분 */
        onEnd: @escaping ()->()
    ) -> some View {

        // why we using overlay or background
        // because it will automatically use the swiftui frame size only!
        return self
            .background {
                HalfSheetManager(
                    sheetView: sheetView,
                    showSheet: shotSheet(),
                    /* 새로 추가한 부분*/
                    onEnd: onEnd
                )
            }
    }
}


/* HALF SHEET MANAGER */

// UIKit Integration
struct HalfSheetManager<SheetView: View>: UIViewControllerRepresentable {
    var sheetView: SheetView
    let controller = UIViewController()
    @Binding var showSheet: Bool

    /* 새로 추가한 부분 */
    var onEnd: () -> ()

    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self)
    }

    func makeUIViewController(context: Context) -> UIViewController {
        controller.view.backgroundColor = .clear
        return controller
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        if showSheet {
            let sheetController = CustomHostingController(rootView: sheetView)
            sheetController.presentationController?.delegate = context.coordinator
            uiViewController.present(sheetController, animated: true)
        } else {
            // choosing view when showsheet toggled againg
            uiViewController.dismiss(animated: true)
        }
    }

    /* 새로 추가한 부분 */
    // On dismiss
    class Coordinator: NSObject, UISheetPresentationControllerDelegate {
        var parent: HalfSheetManager

        init(parent: HalfSheetManager) {
            self.parent = parent
        }

        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            parent.showSheet = false
            parent.onEnd()
        }
    }
}


/* CUSTOM HOSTING CONTROLLER */

// Custom UIHostingController for halfsheet
class CustomHostingController<Content: View>: UIHostingController<Content> {

    override func viewDidLoad() {
        view.backgroundColor = .clear
        // setting presentation controller properties
        if let presentationController = presentationController as? UISheetPresentationController {
            presentationController.detents = [
                .medium(),
                .large()
            ]

            // to show grab protion
            presentationController.prefersGrabberVisible = true
        }
    }
}

 

 


 

reference

  • https://www.youtube.com/watch?v=rQKT7tn4uag

 

GitSpace에 적용하기

  • TargetUserProfileView
    • @State var showSheet: Bool
    • halfSheet
  • View Extension
    • func halfSheet() { return self }
  • HalfSheetManager
    • UIViewControllerPresentable
    • CustomHostingController
    • Coordinator
      • -Dismiss
  • CustomHostingController
    • viewDidLoad
      • UISheetPresentationController

 

 

 

  Comments,     Trackbacks