UIKit x SwiftUI 混编如何 Dismiss HostController?

2,080 阅读3分钟

Hi 👋

我的个人项目扫雷Elic 无尽天梯梦见账本
类型游戏财务
AppStoreElicUmemi

前言

前段时间使用SwiftUI重构了自己的独立项目梦见账本的商店页面。

项目主体是基于UIKit的,这这里的商店页面是Present出一个SwiftUI页面容器UIHostingController

当需要Dismiss该控制器的时候遇到了麻烦,不知道怎么关掉。

梦见账本

一:在SwiftUI中Dismiss一个页面的正常操作

下面的实现方式在正常的SwiftUI体系下是可以正常工作的 但是在UIKit混编时就失效了

struct ContentView: View, SwiftUIBridgeProtocol {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    
    var body: some View {
        ZStack {
            VStack {
                StoreTopView(dismissClosure: {
                    presentationMode.wrappedValue.dismiss()
                })
                .padding()
            }
        }
    }
}

我们看下打印的信息:

(lldb) po presentationMode
 Binding<PresentationMode>
   transaction : Transaction
     plist : []
      - elements : nil
   location : <LocationBox<FunctionalLocation<PresentationMode>>: 0x2838313c0>
   _value : PresentationMode
    - isPresented : false

(lldb) po presentationMode.wrappedValue
 PresentationMode
  - isPresented : false

二:如何才能获取到HostController

2.1 方案一

下面的文正提供了一些尝试的方案,但不够SwiftUI风格

Controlling UIHostingController with SwiftUI View

2.2 方案二 @Environment

通过一段时间的SwiftUI学习,发现通过@Environment可以获取到很多当前页面的一些有用信息。 但是通过观察可用的字段并没有发现有关HostController的线索。

2.3 那是否可以添加自定义字段呢?

可以! 查看EnvironmentKey相关的文档或者注释我们可以发现,其中已经提供了自定义的示例代码:

You can create custom environment values by extending the EnvironmentValues structure with new properties. First declare a new environment key type and specify a value for the required defaultValue property:

private struct MyEnvironmentKey: EnvironmentKey {
    static let defaultValue: String = "Default value"
}

The Swift compiler automatically infers the associated Value type as the type you specify for the default value. Then use the key to define a new environment value property:

extension EnvironmentValues {
    var myCustomValue: String {
        get { self[MyEnvironmentKey.self] }
        set { self[MyEnvironmentKey.self] = newValue }
    }
}

Clients of your environment value never use the key directly. Instead, they use the key path of your custom environment value property. To set the environment value for a view and all its subviews, add the environment(::) view modifier to that view:

MyView()
    .environment(\.myCustomValue, "Another string")

As a convenience, you can also define a dedicated view modifier to apply this environment value:

extension View {
    func myCustomValue(_ myCustomValue: String) -> some View {
        environment(\.myCustomValue, myCustomValue)
    }
}

This improves clarity at the call site:

MyView()
    .myCustomValue("Another string")

To read the value from inside MyView or one of its descendants, use the Environment property wrapper:

struct MyView: View {
    @Environment(\.myCustomValue) var customValue: String

    var body: some View {
        Text(customValue) // Displays "Another value".
    }
}

三:下面按照上面的@Environment思路实现一下

3.1 用以承载ViewController的容器

public struct ViewControllerHolder {
    public weak var value: UIViewController?
    init(_ value: UIViewController?) {
        self.value = value
    }
}

3.2 EnvironmentKey

public struct ViewControllerKey: EnvironmentKey {
    public static var defaultValue: ViewControllerHolder { return ViewControllerHolder(nil) }
}

3.3 Extension

extension EnvironmentValues {
    public var viewController: ViewControllerHolder {
        get { return self[ViewControllerKey.self] }
        set { self[ViewControllerKey.self] = newValue }
    }
}

3.4 打开页面


extension UIViewController {
    public func present<Content: View>(presentationStyle: UIModalPresentationStyle = .automatic, transitionStyle: UIModalTransitionStyle = .coverVertical, animated: Bool = true, completion: @escaping () -> Void = {}, @ViewBuilder builder: () -> Content) {
        let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
        toPresent.modalPresentationStyle = presentationStyle
        toPresent.rootView = AnyView(
            builder()
                .environment(\.viewController, ViewControllerHolder(toPresent))
        )
        if presentationStyle == .overCurrentContext {
            toPresent.view.backgroundColor = .clear
        }
        self.present(toPresent, animated: animated, completion: completion)
    }
}

3.5 关闭页面

struct ShopContentView: View {
...
    // MARK: - Body
    var body: some View {
        ZStack(alignment: .top) {
            ...
            viewControllerHolder.value?.dismiss(animated: false, completion: nil)
            ...
        })
    }
    
...
    // MARK: -  Var: Environment
    @Environment(\.viewController) var viewControllerHolder
...
}

3.6 SwiftUIEx

这里我进行了封装SwiftUIEx,觉得有用欢迎留下一颗⭐️~

思考

通过@Environment实现Dismiss,了解了其更灵活的使用方式,也使解决方式更加SwiftUI风格

👋

My apps

-扫雷Elic 无尽天梯梦见账本
类型游戏财务
AppStoreElicUmemi