到现在为止,你已经听说过迅捷。。。这种新的,声明性的,并且“非常简单”的方式为苹果设备构建用户界面。自去年发布以来,苹果开发社区一直充斥着文章、播客、教程和许多其他资源,这些都在放大宣传。根据Apple,它有可能成为“你编写过的最强大的UI代码”,那么我们都在等什么呢?让我们选择迅捷,永不回头。。。但话说回来,为什么不两个都选呢?

像许多长期存在的应用程序一样,OkCupid的用户可以使用几188bet金宝搏官网个不同的iOS版本。虽然旧版本的用户比例很小,但这并不是不可忽视的。使用SwiftUI重写用户界面所需的工作时间也是如此。还有一些重要的零碎的东西需要考虑,比如SwiftUI没有一个与之相当的UICollectionView视图,一个在OkCupid应用程序(以及其他许多应用程序)中大量使用的核心组件。188bet金宝搏官网

考虑到这些不同的因素,本文将探讨如何在不放弃对iOS12及更老版本的支持的情况下合理地集成SwiftUI,以及首次在SwiftUI中构建所获得的经验教训。在OkCupid,我们希望开始有选择地选择某些特性,使用UIKit和SwiftUI并用共享的业务逻辑代码并行构建。考虑到这一点,我创建了一个名为LoginSample的Xcode项目来演示一个普通用户登录应用程序的过程。你可以自由地跟着克隆188bet金宝搏官网GitHub上的项目回购. 为了简单起见,使用regex将服务器的身份验证替换为本地验证。与OkCupid应用程序一样,LoginSample支持iOS 10及更高版本。188bet金宝搏官网

目标

One of the two main goals of the project is to utilize shared abstractions when building the user interface, whether it's built using UIKit or SwiftUI. The main example of this is the登录视图Model它提供了配置登录视图所需的大部分值和信息。

另一个主要目标是视图应该看起来nearly当它们在屏幕上呈现时相同,并且行为相同。

login-sample-screenshots

跳进去

在SwiftUI和UIKit之间切换的第一步是创建一个视图控制器,作为SwiftUI或UIKit元素的容器。在这种情况下,是LoginContainerViewController. 容器的主要职责是包含一个子视图控制器,该视图支持登录应用程序。版本检查用于决定要显示哪个子视图控制器:

//登录tainervewcontroller.swift软件重写func viewDidLoad(){超级视图加载()addChildViewController()}私有函数addChildViewController(){如果#可用(iOS 13,*){让loginView=loginView(委托:委托)添加(UIHostingController(rootView:loginView),框架:视图.框架)}else{add(LoginViewController(delegate:delegate),框架:视图.框架) } }

上面的代码非常简单。如果用户使用的是iOS 13及更高版本,则UI主机控制器可以在根视图为SwiftUI的地方使用登录视图,否则使用LoginView控制器. 就像Apple's documentation states,的UI主机控制器是完美的解决方案“当你想把SwiftUI视图集成到一个UIKit视图层次结构中。”也包括在上面的snippit是一个方便的扩展Sundell的SwifyUIView视图Controller这使得添加子视图控制器变得很容易。

Sharing is caring

慢慢地将SwiftUI与UIKit并排集成需要编写代码somecode twice. It's unavoidable. User interface construction, configuration, and state is coded using its respective framework. However, the values used for configuration can be shared. By sharing data from one source, specifically the登录视图Model, updates only need to be made in one place which maximizes efficiency and minimizes the chance for discrepancy between the views.

除了登录视图Model,的re is the登录查看删除that is adopted by theLoginCoordinatorand passed down to the login view. The other shared dependency is theLoginDataManager,负责与登录验证器to determine whether to create aUserLoginobject or return an error to display to the user. Sharing dependencies can be seen from the beginning of each lifecycle. The initializers for the登录视图以及LoginView控制器几乎相同:

// 登录视图.swift(SwiftUI)初始化(viewModel:LoginViewModel=L)oginViewModelFactory.create创建(),数据管理器:LoginDataManager=.init(),委托:LoginViewDelegate?){ self.view模型=视图模型自我数据管理器=数据管理器自我授权=委托}//登录查看控制器.swift(UIKit)初始化(viewModel:LoginViewModel=LoginViewModelFactory.create创建(),数据管理器:LoginDataManager=.init(),委托:LoginViewDelegate?){ self.view模型=视图模型自我数据管理器=数据管理器自我授权=代表超级初始化(nibName:nil,bundle:nil)}

近距离观察

这个登录视图Model由嵌套视图模型组成,每个模型包含用于配置登录视图的属性值:

struct LoginViewModel { let backgroundColor: UIColor let buttonModel: LoginButtonModel let contentStackModel: LoginStackModel let emailTextEntryModel: LoginTextEntryViewModel let formStackModel: LoginStackModel let imageModel: LoginImageModel let passwordTextEntryModel: LoginTextEntryViewModel let titleModel: LoginTextModel }

While the view model is shared, it's not perfect. Part of the imperfection lies in attempting to simultaneously provide values needed for both frameworks from one source. The view model is the single source of truth but some property values are applied differently. Additionally, there are other values that require different types depending on the framework. This can be seen when looking more closely at theLoginText模型:

// 物流文本模型.swiftstruct LoginTextModel{let font:UIFont let numberOfLines:Int let text:String let textColor:UIColor}//LoginViewCo配置器.swift(UIKit)controller.titleLabel.font= viewModel.titleModel.fontcontroller.titleLabel.number行= viewModel.titleModel.numberOfLines视图模型控制器.titleLabel.text= viewModel.titleModel.text控制器.titleLabel.textColor= viewModel.titleModel.textColor// 登录文本.swift(SwiftUI)文本(视图模型.text).font(字体(视图模型.font作为CTFont)).lineLimit(viewModel.numberOfLines视图).foregroundColor(颜色(viewModel.textColor))

字符串s are easy to share because字符串是两个框架使用的类型。每个视图使用的字体可以共享,但为了使用UIFont字体在SwiftUI中,初始化字体with a Core Text font reference is required. This is accomplished by casting aUIFont字体作为一个CTFont字体,核心文本使用的不透明类型的标识符。Color在SwiftUI中,可以用UIColor公司. 最后,一个UILabel标签expects生产线数量whereas文本uses alineLimit(_:).

而分配的字体可以利用人数-free bridging, there are a few values in other view models that are not straightforward to share like stack view alignment and content mode. These are addressed by creating a convertible protocol for each that has one convert function and default implementation to convert one type to another. This approach is not ideal and I hope to see it evolve as we start implementing features side by side.

这个last bit of sharing worth mentioning is a UI component, the错误警报视图which is subclassed fromUIView视图. 谢天谢地,SwiftUI确实提供了一些内置的互操作性。在这种情况下,它是UIViewRepresentable的,一个允许集成UIView视图在SwiftUI视图层次结构中。错误警报视图can be the specific type returned bymakeUIView(context:)只要是那种类型,UIView视图.

struct ErrorAlert: UIViewRepresentable { let message: String let width: CGFloat func makeUIView(context: Context) -> ErrorAlertView { return ErrorAlertView(message: message, width: width) } func updateUIView(_ uiView: ErrorAlertView, context: Context) { // not in use } }

If this project was a fully functioning application, it's very likely the error view displayed to the user would be the same across all screens, not just the login view. By taking advantage ofUIViewRepresentable的,可以使用相同的错误视图来消除两个框架的重复工作。

Other lessons learned

由于这是我第一次尝试使用SwiftUI构建一些实质性的东西,我学到了一些东西,通常是在一些尝试和错误之后。虽然这个项目最终实现了前面提到的目标,但是有一些细微的差别。例如,一个文本字段在SwiftUI中,可以选择包含占位符文本,但不能为文本指定颜色。如果您并排仔细观察登录视图的两个呈现,您会注意到灰色略有不同。我确实遇到了一个工作,你可以覆盖一个文本但是为了这个练习,我决定。。。无聊的。最终,我对占位符文本颜色的懒散态度并没有延伸到其他需要更多研究、毅力和关注细节的问题上。

向后兼容性

和任何新的框架一样,bug比比皆是(好吧,也许不是“比比皆是”,但我很喜欢头韵)。当我第一次尝试构建和运行LoginSample如果在低于ios13的版本上运行,应用程序就会崩溃。感谢伊琴曼的回答在stack overflow, all I needed to do was add-weak_framework SwiftUIas the value of其他链接器标志under Linking in Build Settings. Problem solved.

向后兼容性所需的另一个步骤是包括@可用attribute for iOS 13 and up to enclosing structs in files importing SwiftUI:

@可用(iOS 13.0.0, *) struct LoginView: View { ... }

过渡

登录视图,显示和隐藏ErrorAlert视图取决于淋浴器布尔属性。淋浴器使用@州属性包装,以允许修改值。修改该值时,SwiftUI将销毁并重新创建视图结构,而不会丢失状态跟踪。ErrorAlert有一个包含自定义转换的自定义修饰符,移动到无容量的边缘使用函数的不对称(插入:删除:)指定插入和删除视图时发生的不同过渡动画。简单地说,当淋浴器is是的,视图从顶部滑入屏幕,当其值为false,视图从屏幕上向上滑动。

// AnyTransition+Extension.swift extension AnyTransition { static var moveTopEdgeInOutWithOpacity: AnyTransition { let insertion = AnyTransition.move(edge: .top) .combined(with: .opacity) let removal = AnyTransition.move(edge: .top) .combined(with: .opacity) return .asymmetric(insertion: insertion, removal: removal) } } // LoginView.swift @State var showError: Bool = false var body: some View { ... if self.showError { // Show or hide error alert view using transition: // `moveTopEdgeInOutWithOpacity` } ... }

淋浴器更改后,这在某种程度上与预期一样工作,但视图在没有转换的情况下在屏幕上跳上跳下:

login-sample-transition-not-working

要使它发挥作用,还缺少两个步骤。第一步是使用显式动画,将更改包装为淋浴器打电话给withAnimation().By changing the value of淋浴器in the animation block, SwiftUI knows to animate any views that have transitions that depend on it. Note thatwithAnimation()正在使用任何时间值更改,否则插入the removal would not work.

// 登录视图.swiftfunc按钮()。。。带动画{自动淋浴器=真}。。。}函数textEntryTextFieldTapped(){。。。带动画{自动淋浴器=false}。。。}

这个second missing step was explicitly setting the锌试剂登录视图view hierarchy. Thanks to斯科特·格里本的回答关于堆栈溢出,我了解了嵌套视图在父视图层次结构中出现和消失的时间锌试剂“视图可以使用一个锌试剂使用另一个锌试剂取决于SwiftUI重画视图的方式。简单地说,过渡正在发生,但你可能看不到它,因为锌试剂不一致。因此,必须显式指定锌试剂of the views within the overall view hierarchy:

var body: some View { ... ZStack(...) { Color(...) .zIndex(0) LoginVStack(...) { ... } .zIndex(1) if self.showError { ErrorAlert(...) ... .zIndex(2) } } ... }

After implementing those two missing steps, the error alert view behaves as expected:

登录示例转换工作

等等

最后,有两个资源对帮助我学习和完成LoginSample项目至关重要。第一个是保罗·哈德逊的100 Days of SwiftUI“免费收集视频、教程、测试等”,第二个是Vadim Bulavin的键盘避免了快速浏览article.

包装

我真的很喜欢我的牙齿入迅捷和编码这个项目。尽管SwiftUI是声明式的,易于阅读,但理解和实现它仍然需要时间。有几次我心想,“这个怎么样?你是怎么做到的?我怎样才能使这个视图与屏幕顶部垂直对齐?”谢天谢地,苹果/Swift社区一直在那里提供帮助。我期待着看到这个框架的发展,尤其是其他开发人员如何使用UIKit并肩构建用户界面。如前所述,您可以查看GitHub上的LoginSample项目. 感谢阅读!