【SwiftUI】fullScreenCoverがあるならhalfScreenCoverがあっても良くない？

# はじめに
iOS15から`UISheetPresentationController`が使用できるようになり、簡単にハーフモーダルを実装できるようになりました。
SwiftUIでも使えるように`halfScreenCover`として実装します。

# 完成形
`fullScreenCover`と全く同じように使えるようにします。
![スクリーンショット 2022-08-31 17.32.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/09072711-85e5-4953-59e1-15ab90352f85.png)

![Simulator Screen Recording - iPhone 12 - 2022-08-31 at 18.11.50.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/8824f61f-73f8-556f-3ed9-7c6a3609a35c.gif)



# 実装
```swift:halfScreenCover
extension View {
    public func halfScreenCover<T: View>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, content: @escaping () -> T) -> some View {
        return modifier(HalfScreenModifier(isPresented: isPresented, contentT: content))
    }
}
```

```swift:HalfScreenModifier
struct HalfScreenModifier<T: View>: ViewModifier {
    
    @Binding private var isPresented: Bool
    private let onDismiss: (() -> Void)?
    private let contentT: () -> T
    
    init(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, contentT: @escaping () -> T) {
        self._isPresented = isPresented
        self.onDismiss = onDismiss
        self.contentT = contentT
    }
    
    func body(content: Content) -> some View {
        ZStack {
            HalfScreenViewController($isPresented) {
                contentT()
            }
            content
        }
    }
}
```

```swift:HalfScreenViewController
struct HalfScreenViewController<T: View>: UIViewRepresentable {
    
    @Binding private var isPresented: Bool
    private let onDismiss: (() -> Void)?
    private let content: () -> T
    
    init(_ isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> T) {
        self._isPresented = isPresented
        self.onDismiss = onDismiss
        self.content = content
    }
    
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        
        let viewController = UIViewController()
        
        let hostingController = UIHostingController(rootView: content())
        
        viewController.addChild(hostingController)
        viewController.view.addSubview(hostingController.view)
        
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        hostingController.view.leftAnchor.constraint(equalTo: viewController.view.leftAnchor).isActive = true
        hostingController.view.topAnchor.constraint(equalTo: viewController.view.topAnchor).isActive = true
        hostingController.view.rightAnchor.constraint(equalTo: viewController.view.rightAnchor).isActive = true
        hostingController.view.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor).isActive = true
        hostingController.didMove(toParent: viewController)
        
        if let sheetController = viewController.presentationController as? UISheetPresentationController {
            // モーダルのサイズ
            sheetController.detents = [.medium()]
            // 初期位置の設定
            sheetController.selectedDetentIdentifier = .medium
            // 背景を暗くする(おそらくタップ範囲の大きさ)
            sheetController.largestUndimmedDetentIdentifier = .large
            // medium時点でスクロールを可能にするか
            sheetController.prefersScrollingExpandsWhenScrolledToEdge = true
            // 上部にある短いバーを表示するか
            sheetController.prefersGrabberVisible = false

            
            // MARK: iPhone
            // prefersEdgeAttachedInCompactHeight
            /// true: ❌
            /// false: ✅
            // widthFollowsPreferredContentSizeWhenEdgeAttached
            /// true: ✅
            /// false: ✅
            // prefersEdgeAttachedInCompactHeight & widthFollowsPreferredContentSizeWhenEdgeAttached
            /// true & true: ❌
            /// true & false: ❌
            /// false & true: ✅
            /// false & false: ✅
            
            // MARK: iPad
            // prefersEdgeAttachedInCompactHeight
            /// true: ✅
            /// false: ✅
            // widthFollowsPreferredContentSizeWhenEdgeAttached
            /// true: ✅
            /// false: ✅
            // prefersEdgeAttachedInCompactHeight & widthFollowsPreferredContentSizeWhenEdgeAttached
            /// true & true: ✅
            /// true & false: ✅
            /// false & true: ✅
            /// false & false: ✅

            sheetController.prefersEdgeAttachedInCompactHeight = false
            sheetController.widthFollowsPreferredContentSizeWhenEdgeAttached = false
            
            // 角丸サイズ
            sheetController.preferredCornerRadius = 20
        }
        
        viewController.presentationController?.delegate = context.coordinator
        
        if isPresented {
            uiView.window?.rootViewController?.present(viewController, animated: true)
        } else {
            uiView.window?.rootViewController?.dismiss(animated: true)
        }
        
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(isPresented: $isPresented)
    }
    
    class Coordinator: NSObject, UISheetPresentationControllerDelegate {
        @Binding private var isPresented: Bool
        private let onDismiss: (() -> Void)?
        
        init(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil) {
            self._isPresented = isPresented
            self.onDismiss = onDismiss
        }
        
        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            isPresented = false
            if let onDismiss = onDismiss {
                onDismiss()
            }
        }
    }
}
```

:::note warn
**注意**
`prefersEdgeAttachedInCompactHeight`を`true`にするとiPhoneの横画面でクラッシュします。
:::

# 使い方
```swift:ContentView
import SwiftUI

struct ContentView: View {
    @State private var isPresented: Bool = false
    var body: some View {
        ZStack {
            Button("開く") {
                isPresented = true
            }
        }
        .halfScreenCover(isPresented: $isPresented, content: {
            ZStack(alignment: .center) {
                Color(UIColor.yellow).edgesIgnoringSafeArea(.all)
                Text("ハーフシート")
            }
        })
    }
}
```

# おわり
ハーフモーダルの高さとか変えられるようになったらいいなぁ