KkevinK / Me

0 stars 0 forks source link

学习七 SwiftUI与UIKit #9

Open KkevinK opened 3 years ago

KkevinK commented 3 years ago

今天讨论SwiftUI与UIkit的联动。

在此项目中,我们将要求用户从其照片库中导入图片。UIKit随附了专用的代码来执行此操作,但尚未移植到SwiftUI,因此我们需要自己编写桥接。

在编写代码之前,您需要了解三件事,所有这些都有点像UIKit 101,但是如果您仅使用SwiftUI,它们对您来说则是全新的东西。

  1. UIKit有一个名为UIView的类,它是布局中所有视图的父类。因此,标签,按钮,文本字段,滑块等等——都是视图。

  2. UIKit有一个名为UIViewController的类,该类旨在保存所有代码以使视图栩栩如生。就像UIView一样,UIViewController具有许多子类,它们可以完成各种工作。

  3. 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 {

  1. 它使类继承自NSObject,NSObject是UIKit中几乎所有内容的父类。NSObject允许Objective-C询问对象在运行时支持什么功能,这意味着图像选择器可以说“嘿,用户选择了图像,您想做什么?”之类的话。
  2. 它使该类符合UIImagePickerControllerDelegate协议,该协议添加了用于检测用户何时选择图像的功能。(NSObject使Objective-C可以检查功能;该协议实际上是它提供的。)
  3. 它使该类符合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)
}