如何在打开UIImagePicker后保留ARKit场景中的节点
解决打开照片库/相机后之前添加的节点消失问题
嘿,这个问题我之前也碰到过!大概率是因为UIImagePickerController弹出后,你的当前视图控制器被系统销毁并重建了(尤其是在iPad上,或者内存紧张的场景下),导致之前添加的节点没有被持久化保存,所以回到页面后就全部消失了。下面是具体的解决思路和代码调整方案:
核心解决思路
我们需要把所有已添加的节点(文本、图片等)的状态持久化保存起来,在视图控制器重新加载时恢复这些节点。
1. 定义节点模型
首先创建一个Node模型类,用来存储每个节点的关键信息:
import UIKit enum NodeType { case text case image } class Node: Codable { var type: NodeType var content: String // 文本内容或图片本地路径 var frame: CGRect init(type: NodeType, content: String, frame: CGRect) { self.type = type self.content = content self.frame = frame } }
2. 保存与恢复节点状态
在你的视图控制器中添加一个数组来存储节点,并重写视图生命周期方法来处理保存和恢复:
class YourViewController: UIViewController { // 存储所有节点的数组 private var nodes: [Node] = [] // 用来添加节点的父视图(假设你有一个这样的视图容器) @IBOutlet weak var canvasView: UIView! override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // 保存当前所有节点状态 saveNodes() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 恢复之前保存的节点 restoreNodes() } private func saveNodes() { // 遍历canvasView上的所有子视图,转换成Node模型 nodes.removeAll() for subview in canvasView.subviews { if let textView = subview as? UITextView { let textNode = Node(type: .text, content: textView.text, frame: textView.frame) nodes.append(textNode) } else if let imageView = subview as? UIImageView { // 把图片保存到本地沙盒,存储路径 if let image = imageView.image, let imagePath = saveImageToLocal(image: image) { let imageNode = Node(type: .image, content: imagePath, frame: imageView.frame) nodes.append(imageNode) } } } // 把nodes数组归档存储到UserDefaults(或者用CoreData/Realm更复杂的存储) if let encodedData = try? JSONEncoder().encode(nodes) { UserDefaults.standard.set(encodedData, forKey: "savedNodes") } } private func restoreNodes() { // 清空当前canvasView的子视图 canvasView.subviews.forEach { $0.removeFromSuperview() } // 从UserDefaults取出保存的节点数据 guard let encodedData = UserDefaults.standard.data(forKey: "savedNodes"), let savedNodes = try? JSONDecoder().decode([Node].self, from: encodedData) else { return } nodes = savedNodes // 重新创建并添加节点到canvasView for node in nodes { switch node.type { case .text: let textView = UITextView(frame: node.frame) textView.text = node.content // 这里添加你之前设置textView的样式代码 canvasView.addSubview(textView) case .image: let imageView = UIImageView(frame: node.frame) imageView.image = UIImage(contentsOfFile: node.content) imageView.contentMode = .scaleAspectFit canvasView.addSubview(imageView) } } } // 辅助方法:保存图片到本地沙盒 private func saveImageToLocal(image: UIImage) -> String? { guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil } let fileName = UUID().uuidString + ".png" let fileURL = documentsDirectory.appendingPathComponent(fileName) if let data = image.pngData() { do { try data.write(to: fileURL) return fileURL.path } catch { print("保存图片失败:\(error)") return nil } } return nil } }
3. 调整你的弹窗与图片选择代码
另外,你的弹窗代码里有个小问题:手动调用alert.dismiss(animated: true, completion: nil)是多余的,因为present(picker, animated: true)会自动把当前的alert dismiss掉。同时,在图片选择完成后,要记得把新的图片节点添加到nodes数组并保存:
// 实现UIImagePickerControllerDelegate方法 extension YourViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { picker.dismiss(animated: true) { [weak self] in guard let self = self, let editedImage = info[.editedImage] as? UIImage else { return } // 创建图片视图(这里可以自定义位置和大小) let imageView = UIImageView(frame: CGRect(x: 50, y: 50, width: 200, height: 200)) imageView.image = editedImage imageView.contentMode = .scaleAspectFit self.canvasView.addSubview(imageView) // 保存新的图片节点到数组 if let imagePath = self.saveImageToLocal(image: editedImage) { let imageNode = Node(type: .image, content: imagePath, frame: imageView.frame) self.nodes.append(imageNode) self.saveNodes() // 立即保存 } } } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true) } } // 调整你的弹窗代码 func showActionSheet() { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: "Add Text", style: .default , handler:{ [weak self] _ in guard let self = self else { return } self.addTextView.isHidden = false self.inputTextField.becomeFirstResponder() // 注意:当用户输入完成添加文本节点后,也要把文本节点添加到nodes数组并调用saveNodes() })) alert.addAction(UIAlertAction(title: "Choose from Library", style: .default , handler:{ [weak self] _ in guard let self = self else { return } let picker = UIImagePickerController() picker.delegate = self picker.allowsEditing = true picker.sourceType = .photoLibrary self.present(picker, animated: true) })) alert.addAction(UIAlertAction(title: "Take a Photo", style: .default , handler:{ [weak self] _ in guard let self = self else { return } let picker = UIImagePickerController() picker.delegate = self picker.allowsEditing = true picker.sourceType = .camera self.present(picker, animated: true) })) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) // 适配iPad的弹窗展示 if let popoverController = alert.popoverPresentationController { popoverController.sourceView = self.view popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0) } self.present(alert, animated: true) }
额外注意点
- 如果你是在
addTextView的输入完成后添加文本节点,一定要记得把文本节点的信息添加到nodes数组并调用saveNodes()方法。 - 存储方式如果觉得UserDefaults不够安全或容量小,可以换成CoreData或者Realm来存储节点数据。
- 适配iPad的弹窗,避免崩溃(上面的代码已经添加了popover的处理)。
内容的提问来源于stack exchange,提问作者Ruslan Pitula
相关产品推荐
相关产品推荐

