iOS中的容器用于组织其他视图。容器的一个很好的例子是NavigationStack
及其来自Uikit UINavigationContainer
的对应物。
容器本身不会为用户显示任何有用的内容。他们的主要目标是以某种特定的方式展示用户的内容。例如,NavigationStack
显示了最高视图,用导航栏对其进行装饰,并提供逻辑来来回导航。
Martin Barreto的creating reusable container views视频激发了我在Swiftui中创建容器的不同方法深入研究。在视频中,马丁节目的方式如何使用普通的Swiftui创建一个容器。在这篇文章中,我展示了与Uikit互动如何帮助我们节省时间并简化代码。
为了使事情尽可能简单,我们创建了NotificationView
。该容器可以显示我们的界面并使用音乐应用程序样式通知进行装饰。
您可能会在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
}
}
显示通知
让我们将此任务分为两个。
- 显示带有通知内容的
UIViewController
s。 - 提供此
UIViewController
s并计算其内容的大小。
第一任务是安静的直接。我们应该添加子视图控制器,布局的视图和动画过渡。因此,让我们向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应用程序中使用它。即使此组件与自定义标签栏,页面视图或自定义演示文稿和过渡一样大。