Сделать фрейм поставщика предварительного просмотра UIKit ContextMenu соответствующим представлению SwiftUI внутри UIHostingControler

#swiftui #uikit #contextmenu

#swiftui #uikit #contextmenu

Вопрос:

Я изучаю Swift amp; SwiftUI как хобби без опыта UIKit, поэтому я не уверен, возможно ли это в настоящее время. Мне бы очень хотелось использовать контекстные меню UIKit с SwiftUI (например, для реализации подменю, атрибутов действий и, возможно, пользовательских поставщиков предварительного просмотра).

Моя первоначальная идея состояла в том, чтобы создать LegacyContextMenuView with UIViewControllerRepresentable . Затем я бы использовал a UIHostingController для добавления представления SwiftUI как дочернего элемента a UIViewController ContainerViewController , к которому я бы добавил a UIContextMenuInteraction .

Мое текущее решение вроде как работает, но при активации контекстного меню фрейм предварительного просмотра представления ContainerViewController не соответствует размеру UIHostingController представления. Я не знаком с системой компоновки UIKit, поэтому я хотел бы знать:

  1. Возможно ли добавить такие ограничения во время активации предварительного просмотра?
  2. Возможно ли сохранить clipShape базовое представление SwiftUI внутри поставщика предварительного просмотра?

Код:

 // MARK: - Describes a UIKit Context Menu
struct LegacyContextMenu {
    let title: String
    let actions: [UIAction]
    var actionProvider: UIContextMenuActionProvider {
        { _ in
            UIMenu(title: title, children: actions)
        }
    }
    
    init(actions: [UIAction], title: String = "") {
        self.actions = actions
        self.title = title
    }
}

// MARK: - A View that brings UIKit context menus into the SwiftUI world
struct LegacyContextMenuView<Content: View>: UIViewControllerRepresentable {
    let content: Content
    let menu: LegacyContextMenu
    
    func makeUIViewController(context: Context) -> UIViewController {
        let controller = ContainerViewController(rootView: content)
        
        let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
        controller.view.addInteraction(menuInteraction)
        
        return controller
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
    
    func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
    
    class Coordinator: NSObject, UIContextMenuInteractionDelegate {
        let parent: LegacyContextMenuView
        
        init(parent: LegacyContextMenuView) { self.parent = parent }
        
        func contextMenuInteraction(
            _ interaction: UIContextMenuInteraction,
            configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration?
        {
            // previewProvider nil = using the default UIViewController: ContainerViewController
            UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: parent.menu.actionProvider)
        }
    }
    
    class ContainerViewController: UIViewController {
        let hostingController: UIHostingController<Content>
        
        init(rootView: Content) {
            self.hostingController = UIHostingController(rootView: rootView)
            super.init(nibName: nil, bundle: nil)
        }
        
        required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            setupHostingController()
            setupContraints()
            
            // Additional setup required?
        }
        
        func setupHostingController() {
            addChild(hostingController)
            view.addSubview(hostingController.view)
            hostingController.didMove(toParent: self)
        }
        
        // Not familiar with UIKit's layout system so unsure if this is the best approach
        func setupContraints() {
            hostingController.view.translatesAutoresizingMaskIntoConstraints = false
            view.addConstraints([
                hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
                hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
                hostingController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
                hostingController.view.rightAnchor.constraint(equalTo: view.rightAnchor)
            ])
        }
    }
}

// MARK: - Simulate SwiftUI syntax
extension View {
    func contextMenu(_ legacyContextMenu: LegacyContextMenu) -> some View {
        self.modifier(LegacyContextViewModifier(menu: legacyContextMenu))
    }
}

struct LegacyContextViewModifier: ViewModifier {
    let menu: LegacyContextMenu
    
    func body(content: Content) -> some View {
        LegacyContextMenuView(content: content, menu: menu)
    }
}
 

Затем для тестирования я использую это:

 // MARK - A sample view with custom content shape and a dynamic frame
struct SampleView: View {
    @State private var isLarge = false
    
    let viewClipShape = RoundedRectangle(cornerRadius: 50.0)
    
    var body: some View {
        ZStack {
            Color.blue
            Text(isLarge ? "Large" : "Small")
                .foregroundColor(.white)
                .font(.largeTitle)
        }
        .onTapGesture { isLarge.toggle() }
        .clipShape(viewClipShape)
        .contentShape(viewClipShape)
        .frame(height: isLarge ? 250 : 150)
        .animation(.easeInOut, value: isLarge)
    }
}

struct ContentView: View {
    var body: some View {
        SampleView()
            .contextMenu(LegacyContextMenu(actions: [sampleAction], title: "My Menu"))
            .padding(.horizontal)
    }
    
    let sampleAction = UIAction(
            title: "Remove",
            image: UIImage(systemName: "trash.fill"),
            identifier: nil,
            attributes: UIMenuElement.Attributes.destructive,
            handler: { _ in print("Pressed 'Remove'") })
}
 

При длительном нажатии анимация масштабирования контекстного меню учитывает форму содержимого SampleView как для малых, так и для больших размеров, но предварительный просмотр отображается следующим образом:

Маленький

Большой

Ответ №1:

вы должны установить preferredContentSize ViewController, чтобы соответствовать размеру содержимого, который вы хотите