Open KkevinK opened 3 years ago
今天讨论SwiftUI与UIkit的联动。
在此项目中,我们将要求用户从其照片库中导入图片。UIKit随附了专用的代码来执行此操作,但尚未移植到SwiftUI,因此我们需要自己编写桥接。
在编写代码之前,您需要了解三件事,所有这些都有点像UIKit 101,但是如果您仅使用SwiftUI,它们对您来说则是全新的东西。
UIKit有一个名为UIView的类,它是布局中所有视图的父类。因此,标签,按钮,文本字段,滑块等等——都是视图。
UIKit有一个名为UIViewController的类,该类旨在保存所有代码以使视图栩栩如生。就像UIView一样,UIViewController具有许多子类,它们可以完成各种工作。
UIKit使用一种称为委托的设计模式来确定工作在哪里进行。因此,在决定我们的代码如何响应文本字段的值更改时,我们将使用我们的功能创建一个自定义类,并让我们成为文本字段的委托。
包装UIKit视图控制器要求我们创建一个符合UIViewControllerRepresentable协议的结构体。这是基于View的,这意味着我们定义的结构可以在SwiftUI视图层次结构体内使用,但是我们没有提供body属性,因为视图的主体是视图控制器本身——它仅显示UIKit传回的内容。
struct ImagePicker: UIViewControllerRepresentable { func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController { let picker = UIImagePickerController() return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) { } }
makeUIViewController和updateUIViewController是两个必须的方法,在视图加载和变更时执行,随后去swiftui中使用
struct ContentView: View { @State private var image: Image? @State private var showingImagePicker = false var body: some View { VStack { image? .resizable() .scaledToFit() Button("Select Image") { self.showingImagePicker = true } } .sheet(isPresented: $showingImagePicker) { ImagePicker() } } }
在sheet中直接使用了ImagePicker,因为它是有效的SwiftUI视图,这意味着我们现在可以像其他任何SwiftUI视图一样在工作表中显示它。
当您点击按钮时,默认的UIKit图像选择器应向上滑动,以浏览所有照片,而当您选择其中一张时,它将消失。尽管我们刚刚选择了一张图像,但在我们的视图中不会出现任何图像。您会看到,即使我们在视图中放置了SwiftUI图像,也没有将UIImagePickerController中的选择分配给它的任何地方。为了实现这一目标,需要一个全新的概念:coordinators。
coordinators是一个协调器,旨在充当UIKit视图控制器的委托。请记住,“委托”是对其他地方发生的事件做出响应的对象。例如,UIKit使我们可以将委托对象附加到其文本字段视图,当用户键入任何内容,按回车键等等时,该委托将得到通知。这意味着UIKit开发人员可以修改其文本字段的行为方式,而不必创建自己的自定义文本字段类型。
struct ImagePicker: UIViewControllerRepresentable { @Binding var image: UIImage? @Environment(\.presentationMode) var presentationMode func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController { //let picker = UIImagePickerController() let picker = PickerController() picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) { } class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { var parent: ImagePicker init(_ parent: ImagePicker) { self.parent = parent } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let uiImage = info[.originalImage] as? UIImage { parent.image = uiImage } parent.presentationMode.wrappedValue.dismiss() } } } class PickerController: UIImagePickerController { override func viewDidLoad() { super.viewDidLoad() print("Now Loading!") } }
即使该类位于UIViewControllerRepresentable结构体中,SwiftUI也不会自动将其用于视图的协调器。相反,我们需要添加一个名为makeCoordinator()的新方法,如果实现该方法,SwiftUI将自动调用该方法。所有这些需要做的就是创建并配置我们的Coordinator类的实例,然后将其返回。
下一步是告诉UIImagePickerController,当发生某些事情时,应该告诉我们的协调器。
picker.delegate = context.coordinator
我们不会自己调用makeCoordinator()。创建ImagePicker的实例时,SwiftUI会自动调用它。甚至更好的是,SwiftUI自动将其创建的协调器与我们的ImagePicker结构体相关联,这意味着当它调用makeUIViewController()和updateUIViewController()时,它将自动将该协调器对象传递给我们。
因此,我们刚刚编写的代码行告诉Swift使用刚刚制作的协调器作为UIImagePickerController的委托。这意味着在图像选择器控制器内部发生任何事情时(即,当用户选择图像时),它将向我们的协调器报告该操作。 我们的代码无法编译的原因是,Swift正在检查我们的协调器类是否能够充当UIImagePickerController的委托,发现它没有,因此拒绝进一步构建我们的代码。要解决此问题,我们需要从我们原来的Coordinator类修改为:
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
现在可以看到为什么需要为Coordinator使用类:我们需要从NSObject继承,以便Objective-C可以查询我们的协调器以查看其支持的功能。
至此,我们有了一个包装了UIImagePickerController的ImagePicker结构体,并且我们已经配置了该图像选择器控制器,以便在发生有趣的事情时与我们的Coordinator类进行对话。
UIImagePickerControllerDelegate协议定义了我们可以实现的两种可选方法:一种用于用户选择图像时,另一种用于用户按下cancel时。如果我们不实现cancel方法,则UIKit会自动关闭图像选择器控制器,因此我们可以不必理会。但是“选择图像”方法很重要:我们需要抓住它并对该图像做些事情。
我们想要的是ImagePicker将该图像报告回最初使用选择器的对象。我们将在ContentView结构体中的工作表内显示ImagePicker,因此我们希望将所选的图像都提供给它,然后关闭工作表。使用@Binding属性包装器:
@Binding var image: UIImage?
我们还希望在选择图片后关闭该视图。现在,我们根本不处理图像选择,因此我们获得了UIKit的默认关闭行为,但是一旦注入一些自定义功能,我们就需要手动处理关闭。
因此,将第二个属性添加到ImagePicker,以便我们可以以编程方式关闭视图:
@Environment(\.presentationMode) var presentationMode
然后我们需要在Coordinator类中使用它们。告诉协调器其父级是什么是更好的选择,以便它可以在那里直接修改值:
var parent: ImagePicker init(_ parent: ImagePicker) { self.parent = parent }
最后,我们准备好实际读取UIImagePickerController的响应,这是通过实现一个具有非常特定名称的方法来完成的。由于UIKit是图像选择器的委托,因此UIKit将在我们的Coordinator类中查找此方法,如果找到该方法,则将对其进行调用。
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let uiImage = info[.originalImage] as? UIImage { parent.image = uiImage } parent.presentationMode.wrappedValue.dismiss() }
该方法接收一个字典,其中键的类型为UIImagePickerController.InfoKey,而值的类型为Any。我们的工作就是挖掘这些信息,以找到选定的图像,将其分配给我们的父级,然后关闭图像选择器。
在contentView中添加loadImage方法并修改sheet:
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) { func loadImage() { guard let inputImage = inputImage else { return } image = Image(uiImage: inputImage) }
我们创建了一个符合UIViewControllerRepresentable的SwiftUI视图。
我们给了它一个makeUIViewController()方法,该方法创建了某种UIViewController,在我们的示例中为UIImagePickerController。
我们添加了一个嵌套的Coordinator类,以充当UIKit视图控制器和SwiftUI视图之间的桥梁。
我们为协调器提供了didFinishPickingMediaWithInfo方法,该方法将在选择图像时由UIKit触发。
最后,我们为ImagePicker提供了@Binding属性,以便它可以将更改传递给父视图。
今天讨论SwiftUI与UIkit的联动。
在此项目中,我们将要求用户从其照片库中导入图片。UIKit随附了专用的代码来执行此操作,但尚未移植到SwiftUI,因此我们需要自己编写桥接。
在编写代码之前,您需要了解三件事,所有这些都有点像UIKit 101,但是如果您仅使用SwiftUI,它们对您来说则是全新的东西。
UIKit有一个名为UIView的类,它是布局中所有视图的父类。因此,标签,按钮,文本字段,滑块等等——都是视图。
UIKit有一个名为UIViewController的类,该类旨在保存所有代码以使视图栩栩如生。就像UIView一样,UIViewController具有许多子类,它们可以完成各种工作。
UIKit使用一种称为委托的设计模式来确定工作在哪里进行。因此,在决定我们的代码如何响应文本字段的值更改时,我们将使用我们的功能创建一个自定义类,并让我们成为文本字段的委托。
包装UIKit视图控制器要求我们创建一个符合UIViewControllerRepresentable协议的结构体。这是基于View的,这意味着我们定义的结构可以在SwiftUI视图层次结构体内使用,但是我们没有提供body属性,因为视图的主体是视图控制器本身——它仅显示UIKit传回的内容。
makeUIViewController和updateUIViewController是两个必须的方法,在视图加载和变更时执行,随后去swiftui中使用
在sheet中直接使用了ImagePicker,因为它是有效的SwiftUI视图,这意味着我们现在可以像其他任何SwiftUI视图一样在工作表中显示它。
当您点击按钮时,默认的UIKit图像选择器应向上滑动,以浏览所有照片,而当您选择其中一张时,它将消失。尽管我们刚刚选择了一张图像,但在我们的视图中不会出现任何图像。您会看到,即使我们在视图中放置了SwiftUI图像,也没有将UIImagePickerController中的选择分配给它的任何地方。为了实现这一目标,需要一个全新的概念:coordinators。
coordinators是一个协调器,旨在充当UIKit视图控制器的委托。请记住,“委托”是对其他地方发生的事件做出响应的对象。例如,UIKit使我们可以将委托对象附加到其文本字段视图,当用户键入任何内容,按回车键等等时,该委托将得到通知。这意味着UIKit开发人员可以修改其文本字段的行为方式,而不必创建自己的自定义文本字段类型。
即使该类位于UIViewControllerRepresentable结构体中,SwiftUI也不会自动将其用于视图的协调器。相反,我们需要添加一个名为makeCoordinator()的新方法,如果实现该方法,SwiftUI将自动调用该方法。所有这些需要做的就是创建并配置我们的Coordinator类的实例,然后将其返回。
下一步是告诉UIImagePickerController,当发生某些事情时,应该告诉我们的协调器。
picker.delegate = context.coordinator
我们不会自己调用makeCoordinator()。创建ImagePicker的实例时,SwiftUI会自动调用它。甚至更好的是,SwiftUI自动将其创建的协调器与我们的ImagePicker结构体相关联,这意味着当它调用makeUIViewController()和updateUIViewController()时,它将自动将该协调器对象传递给我们。
因此,我们刚刚编写的代码行告诉Swift使用刚刚制作的协调器作为UIImagePickerController的委托。这意味着在图像选择器控制器内部发生任何事情时(即,当用户选择图像时),它将向我们的协调器报告该操作。 我们的代码无法编译的原因是,Swift正在检查我们的协调器类是否能够充当UIImagePickerController的委托,发现它没有,因此拒绝进一步构建我们的代码。要解决此问题,我们需要从我们原来的Coordinator类修改为:
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
现在可以看到为什么需要为Coordinator使用类:我们需要从NSObject继承,以便Objective-C可以查询我们的协调器以查看其支持的功能。
至此,我们有了一个包装了UIImagePickerController的ImagePicker结构体,并且我们已经配置了该图像选择器控制器,以便在发生有趣的事情时与我们的Coordinator类进行对话。
UIImagePickerControllerDelegate协议定义了我们可以实现的两种可选方法:一种用于用户选择图像时,另一种用于用户按下cancel时。如果我们不实现cancel方法,则UIKit会自动关闭图像选择器控制器,因此我们可以不必理会。但是“选择图像”方法很重要:我们需要抓住它并对该图像做些事情。
我们想要的是ImagePicker将该图像报告回最初使用选择器的对象。我们将在ContentView结构体中的工作表内显示ImagePicker,因此我们希望将所选的图像都提供给它,然后关闭工作表。使用@Binding属性包装器:
@Binding var image: UIImage?
我们还希望在选择图片后关闭该视图。现在,我们根本不处理图像选择,因此我们获得了UIKit的默认关闭行为,但是一旦注入一些自定义功能,我们就需要手动处理关闭。
因此,将第二个属性添加到ImagePicker,以便我们可以以编程方式关闭视图:
@Environment(\.presentationMode) var presentationMode
然后我们需要在Coordinator类中使用它们。告诉协调器其父级是什么是更好的选择,以便它可以在那里直接修改值:
最后,我们准备好实际读取UIImagePickerController的响应,这是通过实现一个具有非常特定名称的方法来完成的。由于UIKit是图像选择器的委托,因此UIKit将在我们的Coordinator类中查找此方法,如果找到该方法,则将对其进行调用。
该方法接收一个字典,其中键的类型为UIImagePickerController.InfoKey,而值的类型为Any。我们的工作就是挖掘这些信息,以找到选定的图像,将其分配给我们的父级,然后关闭图像选择器。
在contentView中添加loadImage方法并修改sheet:
我们创建了一个符合UIViewControllerRepresentable的SwiftUI视图。
我们给了它一个makeUIViewController()方法,该方法创建了某种UIViewController,在我们的示例中为UIImagePickerController。
我们添加了一个嵌套的Coordinator类,以充当UIKit视图控制器和SwiftUI视图之间的桥梁。
我们为协调器提供了didFinishPickingMediaWithInfo方法,该方法将在选择图像时由UIKit触发。
最后,我们为ImagePicker提供了@Binding属性,以便它可以将更改传递给父视图。