3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-31

はじめに

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

完成形

fullScreenCoverと全く同じように使えるようにします。
スクリーンショット 2022-08-31 17.32.39.png

Simulator Screen Recording - iPhone 12 - 2022-08-31 at 18.11.50.gif

実装

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))
    }
}
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
        }
    }
}
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()
            }
        }
    }
}

注意
prefersEdgeAttachedInCompactHeighttrueにするとiPhoneの横画面でクラッシュします。

使い方

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("ハーフシート")
            }
        })
    }
}

おわり

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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?