Swiftui的高级容器
#ios #swift #swiftui #uikit

iOS中的容器用于组织其他视图。容器的一个很好的例子是NavigationStack及其来自Uikit UINavigationContainer的对应物。

容器本身不会为用户显示任何有用的内容。他们的主要目标是以某种特定的方式展示用户的内容。例如,NavigationStack显示了最高视图,用导航栏对其进行装饰,并提供逻辑来来回导航。

Martin Barretocreating reusable container views视频激发了我在Swiftui中创建容器的不同方法深入研究。在视频中,马丁节目的方式如何使用普通的Swiftui创建一个容器。在这篇文章中,我展示了与Uikit互动如何帮助我们节省时间并简化代码。

为了使事情尽可能简单,我们创建了NotificationView。该容器可以显示我们的界面并使用音乐应用程序样式通知进行装饰。

Notification

您可能会在github repo中看到完整的代码。

容器的API

实例化

让我们以与初始化NavigationStack相同的方式初始化我们的容器。为了控制当前状态,我们将Binding作为参数传递给初始化器。通过绑定,我们可以显示和隐藏通知。

@StateObject private var notificationManager = NotificationManager()

var body: some View {
    NotificationView($notificationManager.current) {
        NavigationStack {
            FruitListView()
        }
        .environmentObject(notificationManager)
    }
}

NavigationStack可以在没有外部状态的情况下实例化。它仍然有用,因为它具有可以用作按钮并隐藏所有状态逻辑的NavigationLink。虽然这是像NavigationStack这样的常见组件的一个不错的设计,但我们的容器不能使用此API,因为在TAP上显示通知并不常见。

注册通知

应用程序的不同部分可以具有其特定类型的通知。此外,可以在不同的模块,时间和团队中开发不同的屏幕。因此,再次让我们使用与NavigationStack使用相同的API。使用navigationDestination(for:destination:)方法,我们可以注册可以在堆栈上推动的不同屏幕。

struct FruitListView: View {
    @EnvironmentObject var notificationManager: NotificationManager

    var body: some View {
        List {
            ForEach(Fruit.allFruits, id: \.emoji) { fruit in
                Button(fruit.name) {
                    notificationManager.value = fruit
                }
            }
        }
        .notification(for: Fruit.self) { fruit in
            // notification content
        }
    }
}

我们为任何类型注册通知,然后可以通过设置绑定值来显示。我们可以注册许多类型的通知。

执行

包裹内容

让我们从一个简单的任务开始,显示内部通知容器内部接口。

struct NotificationViewControllerWrapper<Content: View>: UIViewControllerRepresentable {
    private let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    func makeUIViewController(context: Context) -> NotificationViewController {
        let contentController = UIHostingController(rootView: content)
        let controller = NotificationViewController(content: contentController)
        return controller
    }
}

NotificationViewController是所有演示逻辑所在的实际地方。目前,它只是在没有任何装饰的情况下显示内容。

class NotificationViewController: UIViewController {
    private let content: UIViewController

    init(content: UIViewController) {
        self.content = content
        super.init(nibName: nil, bundle: nil)
        addChild(content)
        content.didMove(toParent: self)
    }

    override func loadView() {
        view = UIView()
        view.addSubview(content.view)
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        content.view.frame = view.bounds
    }
}

如果我们尝试使用全新的视图,我们会发现NavigationStack忽略了安全的区域插图,而我们的容器则不会。解决这个问题可能很棘手。例如,直接方法无用。

// this approach doesn't work
func makeUIViewController(context: Context) -> NotificationViewController {
    let content = content
        .ignoreSafeArea() // <- ignoring safe area
    let contentController = UIHostingController(rootView: content)
    let controller = NotificationViewController(content: contentController)
    return controller
}

为了使我们的视野忽略安全区域,我们应该要求它从外部进行此操作,因此让我们将其包裹在另一种视图中。检查此great article,以更好地了解SwiftUi布局的工作原理。

struct NotificationView<Root: View>: View {
    private let root: () -> Root

    init(@ViewBuilder root: @escaping () -> Root) {
        self.root = root
    }

    var body: some View {
        NotificationViewControllerWrapper(content: root)
            .ignoresSafeArea()
    }
}

注册通知

NotificationView中的任何视图都可以注册其自己的通知。为此,让我们将环境对象发送到层次结构。

struct RegistryModifier<T, Note: View>: ViewModifier {
    @Environment(\.notificationRegistry) var registry

    let note: (T) -> Note

    func body(content: Content) -> some View {
        content
            .onAppear {
                registry?.register(for: T.self, content: note)
            }
            .onDisappear {
                registry?.unregister(T.self)
            }
    }
}

extension View {
    func notification<T, Content: View>(for type: T.Type, @ViewBuilder content: @escaping (T) -> Content) -> some View {
        modifier(RegistryModifier(note: content))
    }
}

func makeUIViewController(context: Context) -> NotificationViewController {
    let content = self.content
        .environment(\.notificationRegistry, context.coordinator.registry) // <- adding to hierarchy
    let contentController = UIHostingController(rootView: content)
    let controller = NotificationViewController(content: contentController)
    return controller
}

但是我们应该发送什么?我们希望有能力添加和删除任何类型的通知。另外,我们不希望注册另一个通知开始视图更新周期。因此,notificationRegistry应该具有一对非突变方法。最简单的方法是使用参考类型。

class NotificationRegistry {
    // AnyView is for simplicity, the better way is to erase type directly to UIViewController
    private var storage = [String: (Any) -> AnyView]()

    func register<T, Content: View>(for type: T.Type, content: @escaping (T) -> Content) {
        let key = String(reflecting: type)
        let value: (Any) -> _ = {
            AnyView(content($0 as! T))
        }
        storage[key] = value
    }

    func unregister<T>(_ type: T.Type) {
        let key = String(reflecting: type)
        storage[key] = nil
    }
}

显示通知

让我们将此任务分为两个。

  1. 显示带有通知内容的UIViewControllers。
  2. 提供此UIViewControllers并计算其内容的大小。

第一任务是安静的直接。我们应该添加子视图控制器,布局的视图和动画过渡。因此,让我们向NotificationViewController添加两种方法。

func removeNotification() {
    // remove a visible notification
}

func showNotification(_ viewController: UIViewController, size: CGSize) {
    // show the new notification or add transition from an old notification to the new one
}

实际上,第二个任务更加简单。如果我们将Coordinator添加到NotificationViewControllerWrapper swiftui将为我们创建它,我们可以使用它来更新视图。让我们在协调器中添加注册表。

class Coordinator {
    let registry = NotificationRegistry()
}

从现在开始,我们可以使用注册表在需要显示时创建通知视图。

让我们向NotificationViewControllerWrapper添加通知值。

private var value: Any?

和注册表方法可以从中获得视图。

class NotificationRegistry {
    private var storage = [String: (Any) -> AnyView]()

    func view(for value: Any) -> AnyView? {
        let key = String(reflecting: type(of: value))
        guard let factory = storage[key] else {
            return nil
        }
        return factory(value)
    }
}

我们准备实现updateUIViewController方法。

func updateUIViewController(_ uiViewController: NotificationViewController, context: Context) {
    if let value = value, let view = context.coordinator.registry.view(for: value) {
        let notificationViewController = UIHostingController(rootView: view)
        let size = notificationViewController.sizeThatFits(in: uiViewController.view.bounds.insetBy(dx: 20, dy: 100).size)
        uiViewController.showNotification(notificationViewController, size: size)
    } else {
        uiViewController.removeNotification()
    }
}

我们使用sizeThatFits(in:) UIHostingController的方法获得了通知视图的实际尺寸。

总之

希望您发现这篇文章有用。在这个简单的示例上,您可以学习如何创建应用程序的根组件。

SwiftUI是一个很棒的UI框架。它的主要功能之一是与Uikit互动。连同环境和偏好之类的数据流功能,它可以帮助我们创建复杂的Uikit组件,并在现代SwiftUI应用程序中使用它。即使此组件与自定义标签栏,页面视图或自定义演示文稿和过渡一样大。