Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Anton Sergeev
Anton Sergeev

Posted on

     

Advanced Containers in SwiftUI

Containers in iOS is used to organize other views. The great example of container isNavigationStack and its counterpart from UIKitUINavigationContainer.

Containers don't show any useful content for a user by itself. Their main goal is to show a user's content in some specific way. For instance,NavigationStack shows a topmost view, decorate it with a navigation bar and provide logic to navigate back and forth.

Acreating reusable container views video byMartin Barreto inspired me to deep dive in different approaches of creating containers in SwiftUI. In the video Martin show's how to create a container with plain SwiftUI. In this post I'm showing how interop with UIKit can help us to save our time and simplify our code.

To make things as simple as possible we createNotificationView. This container can show our interface and decorate it with Music app styled notifications.

Notification

You may see full code in a github repo.

Container's API

Instantiation

Let's initialize our container the same way as we initializeNavigationStack. To control current state we passBinding as a parameter to an initializer. With the binding we can show and hide notifications.

@StateObjectprivatevarnotificationManager=NotificationManager()varbody:someView{NotificationView($notificationManager.current){NavigationStack{FruitListView()}.environmentObject(notificationManager)}}
Enter fullscreen modeExit fullscreen mode

NavigationStack can be instantiate without external state for its stack. It's still useful because it hasNavigationLink that can behave as button and hide all state logic. While this is a nice design for such a common component asNavigationStack, our container can't use this API because it's not common to show notifications on a tap.

Register notifications

Different part of an app can have their specific types of notifications. Furthermore, different screens can be developed in different modules, times and teams. So once again let's use the same API asNavigationStack use. WithnavigationDestination(for:destination:) method we can register different screen that can be pushed on stack.

structFruitListView:View{@EnvironmentObjectvarnotificationManager:NotificationManagervarbody:someView{List{ForEach(Fruit.allFruits,id:\.emoji){fruitinButton(fruit.name){notificationManager.value=fruit}}}.notification(for:Fruit.self){fruitin// notification content}}}
Enter fullscreen modeExit fullscreen mode

We register a notification for any type and after that it can be shown by setting a binding value. We can register many types of notifications.

Implementation

Wrap the content

Let's start with a simple task of showing main interface inside notification container.

structNotificationViewControllerWrapper<Content:View>:UIViewControllerRepresentable{privateletcontent:Contentinit(@ViewBuildercontent:()->Content){self.content=content()}funcmakeUIViewController(context:Context)->NotificationViewController{letcontentController=UIHostingController(rootView:content)letcontroller=NotificationViewController(content:contentController)returncontroller}}
Enter fullscreen modeExit fullscreen mode

NotificationViewController is an actual place where all the presentation logic lies. At the moment it's just showing content without any decoration.

classNotificationViewController:UIViewController{privateletcontent:UIViewControllerinit(content:UIViewController){self.content=contentsuper.init(nibName:nil,bundle:nil)addChild(content)content.didMove(toParent:self)}overridefuncloadView(){view=UIView()view.addSubview(content.view)}overridefuncviewWillLayoutSubviews(){super.viewWillLayoutSubviews()content.view.frame=view.bounds}}
Enter fullscreen modeExit fullscreen mode

If we try to use our brand new view, we'll find out thatNavigationStack ignores safe area insets while our container does not. It's can be tricky to fix this. For instance, the straightforward approach is not useful.

// this approach doesn't workfuncmakeUIViewController(context:Context)->NotificationViewController{letcontent=content.ignoreSafeArea()// <- ignoring safe arealetcontentController=UIHostingController(rootView:content)letcontroller=NotificationViewController(content:contentController)returncontroller}
Enter fullscreen modeExit fullscreen mode

To make our view to ignore safe area we should ask it to do this from the outside, so let's wrap it in another view. Check thisgreat article for better understanding how SwiftUI layout engine works.

structNotificationView<Root:View>:View{privateletroot:()->Rootinit(@ViewBuilderroot:@escaping()->Root){self.root=root}varbody:someView{NotificationViewControllerWrapper(content:root).ignoresSafeArea()}}
Enter fullscreen modeExit fullscreen mode

Register notifications

Any view insideNotificationView can register its own notifications. To do this let's send an environment object down to hierarchy.

structRegistryModifier<T,Note:View>:ViewModifier{@Environment(\.notificationRegistry)varregistryletnote:(T)->Notefuncbody(content:Content)->someView{content.onAppear{registry?.register(for:T.self,content:note)}.onDisappear{registry?.unregister(T.self)}}}extensionView{funcnotification<T,Content:View>(fortype:T.Type,@ViewBuildercontent:@escaping(T)->Content)->someView{modifier(RegistryModifier(note:content))}}funcmakeUIViewController(context:Context)->NotificationViewController{letcontent=self.content.environment(\.notificationRegistry,context.coordinator.registry)// <- adding to hierarchyletcontentController=UIHostingController(rootView:content)letcontroller=NotificationViewController(content:contentController)returncontroller}
Enter fullscreen modeExit fullscreen mode

But what should we send? We want to have ability to add and remove notifications for any type. Also we don't want that registering another notification start view update cycle. SonotificationRegistry should have pair of non mutating methods. The most simple way to do this is using reference type.

classNotificationRegistry{// AnyView is for simplicity, the better way is to erase type directly to UIViewControllerprivatevarstorage=[String:(Any)->AnyView]()funcregister<T,Content:View>(fortype:T.Type,content:@escaping(T)->Content){letkey=String(reflecting:type)letvalue:(Any)->_={AnyView(content($0as!T))}storage[key]=value}funcunregister<T>(_type:T.Type){letkey=String(reflecting:type)storage[key]=nil}}
Enter fullscreen modeExit fullscreen mode

Show notification

Let's split this task in two.

  1. ShowUIViewControllers with notification content.
  2. Provide thisUIViewControllers and calculate size of their content.

First task is quiet straightforward. We should add child view controller, layout its view and animate transition. So let's add two methods toNotificationViewController.

funcremoveNotification(){// remove a visible notification}funcshowNotification(_viewController:UIViewController,size:CGSize){// show the new notification or add transition from an old notification to the new one}
Enter fullscreen modeExit fullscreen mode

In fact the second task is even more simple. If we addCoordinator toNotificationViewControllerWrapper SwiftUI will create it for us and we can use it to update our view. Let's add registry inside coordinator.

classCoordinator{letregistry=NotificationRegistry()}
Enter fullscreen modeExit fullscreen mode

From now we can use the registry to create notification views when they need to be shown.

Let's add notification value toNotificationViewControllerWrapper.

privatevarvalue:Any?
Enter fullscreen modeExit fullscreen mode

And registry method to get a view from it.

classNotificationRegistry{privatevarstorage=[String:(Any)->AnyView]()funcview(forvalue:Any)->AnyView?{letkey=String(reflecting:type(of:value))guardletfactory=storage[key]else{returnnil}returnfactory(value)}}
Enter fullscreen modeExit fullscreen mode

We are ready to implementupdateUIViewController method.

funcupdateUIViewController(_uiViewController:NotificationViewController,context:Context){ifletvalue=value,letview=context.coordinator.registry.view(for:value){letnotificationViewController=UIHostingController(rootView:view)letsize=notificationViewController.sizeThatFits(in:uiViewController.view.bounds.insetBy(dx:20,dy:100).size)uiViewController.showNotification(notificationViewController,size:size)}else{uiViewController.removeNotification()}}
Enter fullscreen modeExit fullscreen mode

We usesizeThatFits(in:) method ofUIHostingController to obtain an actual size of notification view.

In summary

I hope you found this article useful. On this simple example you may learn how to create root components of an app.

SwiftUI is a great UI framework. And one of the its main feature is interop with UIKit. Together with data flow features like environment and preferences it can help us to create complicated UIKit components and use it inside modern SwiftUI apps. Even if this components as big as custom tab bars, page views or custom presentations and transitions.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Joined

Trending onDEV CommunityHot

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp