I'm trying to load different models on face using SwiftUI, RealityKit and ARKit.
struct AugmentedRealityView: UIViewRepresentable {
#Binding var modelName: String
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let configuration = ARFaceTrackingConfiguration()
arView.session.run(configuration, options: [.removeExistingAnchors,
.resetTracking])
loadModel(name: modelName, arView: arView)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) { }
private func loadModel(name: String, arView: ARView) {
var cancellable: AnyCancellable? = nil
cancellable = ModelEntity.loadAsync(named: name).sink(
receiveCompletion: { loadCompletion in
if case let .failure(error) = loadCompletion {
print("Unable to load model: \(error.localizedDescription)")
}
cancellable?.cancel()
},
receiveValue: { model in
let faceAnchor = AnchorEntity(.face)
arView.scene.addAnchor(faceAnchor)
faceAnchor.addChild(model)
model.scale = [1, 1, 1]
})
}
}
This is how I load them but when the camera view opens and loads one model then the other models won't be loaded. Can someone help me out?
When the value of your Binding changes, SwiftUI is calling your updateUIView(_:,context:) implementation, which does noting.
Additionally, you are not storing the AnyCancellable. When the token returned by sink gets deallocated the request will be cancelled. That could result in unexpected failures when trying to load lager models.
To fix both of these issue, use a Coordinator.
import UIKit
import RealityKit
import SwiftUI
import Combine
import ARKit
struct AugmentedRealityView: UIViewRepresentable {
class Coordinator {
private var token: AnyCancellable?
private var currentModelName: String?
fileprivate func loadModel(_ name: String, into arView: ARView) {
// Only load model if the name is different from the previous one
guard name != currentModelName else {
return
}
currentModelName = name
// This is optional
// When the token gets overwritten
// the request gets cancelled
// automatically
token?.cancel()
token = ModelEntity.loadAsync(named: name).sink(
receiveCompletion: { loadCompletion in
if case let .failure(error) = loadCompletion {
print("Unable to load model: \(error.localizedDescription)")
}
},
receiveValue: { model in
let faceAnchor = AnchorEntity(.camera)
arView.scene.addAnchor(faceAnchor)
faceAnchor.addChild(model)
model.scale = [1, 1, 1]
})
}
fileprivate func cancelRequest() {
token?.cancel()
}
}
#Binding var modelName: String
func makeCoordinator() -> Coordinator {
Coordinator()
}
static func dismantleUIView(_ uiView: ARView, coordinator: Coordinator) {
coordinator.cancelRequest()
}
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let configuration = ARFaceTrackingConfiguration()
arView.session.run(configuration, options: [.removeExistingAnchors,
.resetTracking])
context.coordinator.loadModel(modelName, into: arView)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
context.coordinator.loadModel(modelName, into: uiView)
}
}
We create a nested Coordinator class that holds the AnyCancellable token and move the loadModel function into the Coordinator.
Other than a SwiftUI View, the Coordinator is a class that lives while your view is visible (always a remember that SwiftUI might create and destroy your View at will, its lifecycle is not related to the actual "view" that is shown on screen).
In out loadModel class we double check that the value of our Binding actually changed so that we don't cancel an ongoing request for the same model when SwiftUI updates our View, e.g. because of a change in the environment.
Then we implement the makeCoordinator function to construct one of our Coordinator objects.
Both in makeUIView and in updateUIView we call the loadModel function on our Coordinator.
The dimantleUIView method is optional. When the Coordinator gets deconstructed our token gets released as well, which will trigger Combine into canceling ongoing requests.
Related
I am having an issue with my Coordinator. I am interacting with a MKMapView via SwiftUI. I am passing in a Binding to the UIViewRepresentable and need to access that same Binding in the Coordinator. Inside the Coordinator I determine what strokeColor to use for my polyline. When I try to access the routes Binding from my Coordinator it is always empty. When I set a breakpoint inside the MapView on the updateUIView function the binding is indeed populated.
Heres the code:
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
var region: MKCoordinateRegion
#Binding var routes: [RouteData]
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
mapView.setRegion(region, animated: true)
addOverlays(mapView)
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
addOverlays(view)
removeOverlays(view)
}
private func addOverlays(_ view: MKMapView) {
for route in routes {
for point in route.points {
let waypoints = point.waypoints
let polyline = MKPolyline(coordinates: waypoints, count: waypoints.count)
polyline.title = route.routeID
view.addOverlay(polyline)
}
}
}
private func removeOverlays(_ view: MKMapView) {
for overlay in view.overlays {
if let routeID = overlay.title!, routes.first(where: { $0.routeID == routeID }) == nil {
view.removeOverlay(overlay)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
class Coordinator: NSObject, MKMapViewDelegate {
let parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let routePolyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: routePolyline)
// Always prints route is empty even though I set a break point inside the parents' updateUIView func and the route is populated.
print("parents routes: \(self.parent.routes)")
if let title = routePolyline.title, let route = self.parent.routes.first(where: { $0.routeID == title }) {
renderer.strokeColor = UIColor(convertRGBStringToColor(color: route.route.rtclr))
} else {
renderer.strokeColor = UIColor.blue
}
renderer.lineWidth = 5
return renderer
}
return MKOverlayRenderer()
}
}
A few mistakes
#Binding var routes: [RouteData] should be let routes: [RouteData] because you don’t change it so don’t need the write access.
Coordinator(self) Should be Coordinator(), self is an old value the Coordinator should not hang on to.
Subclass MKPolyline to add your colour properties eg https://stackoverflow.com/a/44294417/259521
makeUIView Should return context.coordinator.mapView
addOverlays should only add ones that are not already added. You need to essentially implement a diff in updateUIView.
Update is called after make so no need to add overlays in make.
I’m using UIViewControllerRepresentable for Document picker presentation in swiftUI. The issue is that I'm not able to select the video audio pdf it's all showing in a frozen manner. I need to fix this issues for the iOS 14 and above version. I'm able to select the file by tap-hold and then release its only works with the simulator in real devices it’s not possible for both the simulator and devices except for the file structure, the rest of the documents are in greyed out.
enter image description here
struct DocumentPicker: UIViewControllerRepresentable {
#ObservedObject var chatViewModel: RedesignChatViewModel
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPicker>) -> UIDocumentPickerViewController {
let viewController = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf, .mp3, .audio, .video, .movie, .item])
viewController.shouldShowFileExtensions = true
viewController.allowsMultipleSelection = false
viewController.delegate = context.coordinator
return viewController
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
}
}
extension DocumentPicker {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
var parent: DocumentPicker
var path: String?
init(_ documentPicker: DocumentPicker) {
self.parent = documentPicker
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return }
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
controller.dismiss(animated: true)
}
There are a few mistakes, first you'll need to create a UIViewController and use that to present the UIDocumentPickerViewController. Also you need to change Coordinator(self) to Coordinator() and in makeUIViewController return context.coordinator.myViewController that should be a lazy property. The reason for this is that self you are passing in is immediately out of date because it is a value type. You also need to remove the #ObservedObject and add lets or #Binding vars for your properties. When the repreresentable is init with new values for those properties, updateUIViewController will be called and you can then update the coordinator and view controller with the new values.
I'm trying to implement a camera preview view in SwiftUI, for which I have the following code:
import SwiftUI
import AVFoundation
struct CameraPreview: UIViewRepresentable {
let session: AVCaptureSession
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .gray
let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: session)
videoPreviewLayer.frame = view.bounds
videoPreviewLayer.videoGravity = .resizeAspectFill
videoPreviewLayer.connection?.videoOrientation = .portrait
view.layer.addSublayer(videoPreviewLayer)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
for layer in uiView.layer.sublayers ?? [] {
layer.frame = uiView.bounds
}
}
}
However I do see the gray background view that I set on the view, but it never starts showing the camera output. I've set a AVCaptureVideoDataOutputSampleBufferDelegate class and I can see the frames being captured and processed, yet for some reason it does not start rendering the output.
I have this other snippet that DOES render the output, but it does so by setting the preview layer as the root layer which is what I want to avoid, here's the code that works:
struct CameraPreview: UIViewRepresentable {
let session: AVCaptureSession
func makeUIView(context: Context) -> UIView {
let view = VideoView()
view.backgroundColor = .gray
view.previewLayer.session = session
view.previewLayer.videoGravity = .resizeAspectFill
view.previewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
for layer in uiView.layer.sublayers ?? [] {
layer.frame = uiView.bounds
}
}
class VideoView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var previewLayer: AVCaptureVideoPreviewLayer {
layer as! AVCaptureVideoPreviewLayer
}
}
}
Some examples I found showed I should be able to show the preview like I do in the first example. I've tried initializing the session with inputs before and after the preview view is created and I've gotten the same result. Am I missing anything? am I not retaining the layer or is there a special configuration for the session to look out for? to make it work I simply swap the implementations and the one with the inner class does render?
Any help is really appreciated.
Some resources:
https://nsscreencast.com/episodes/296-camera-capture-preview-layer-sample-buffer
https://www.appcoda.com/avfoundation-swift-guide/
https://developer.apple.com/documentation/vision/recognizing_objects_in_live_capture
I need to use AVFoundation for an app I'm writing. Basically scanning a barcode, and sending that information back to another ViewController. This was pretty easy/straight forward with Swift and UIKit and I had this working.
Now, I launch the ViewController using a sheet (passing in the #State variable so I can dismiss the sheet later):
.sheet(isPresented: $isShowingCamera, content: {
ScanItem(isPresented: self.$isShowingCamera)
})
ScanItem is a UIViewRepresentable
Here are the functions in ScanItem:
func makeCoordinator() -> ScanItem.Coordinator {
return Coordinator(self)
}
public typealias UIViewControllerType = ScanBarcodeViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<ScanItem>) -> ScanBarcodeViewController {
return ScanBarcodeViewController()
}
func updateViewController(_ uiViewController: ScanBarcodeViewController, context: UIViewControllerRepresentableContext<ScanItem>) {
}
Inside I have the required methods and its displaying another UIViewController I created that uses AVFoundation to display the camera, and look for the barcode. Where I believe I need to progress is making the Coordinator handle the AVCaptureMetaData. I have the Coordinator like below:
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
let parent: ScanItem
init(_ parent: ScanItem) {
self.parent = parent
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
let metaDataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
if metaDataObj.stringValue != nil {
self.parent.$metadata = metadataObj.stringValue
self.parent.$isPresented = false
}
}
I think I'm on the right track here. This function gets called normally in the extension of the viewcontroller as AVCaptureMetadataOutputObjectsDelegate. I think I need to set the Coordinator as the delegate, call the function, set some bindable variable (#Bindable var metadata: String) and handle it in the SwiftUI view.
My current errors:
ScanBarcodeViewController (my viewcontoller to load the camera) cannot be constructed because it has no accessible initializers
Which goes along with
class ScanBarcodeViewController has not initializers
self.parent.$metaData = metaDataObj.stringValue -> cannot assign value of type String to type Binding -- fixed
self.parent.$isPresented = false -> cannot assign value of type Bool to type Binding -- fixed
Instead of passing self in Coordinator (which is struct, so copied), ie
Coordinator(self)
use binding to your model directly, so you can modify it, ie like
func makeCoordinator() -> Coordinator {
return Coordinator(data: $metadata)
}
and in Coordinator..
final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
var data: Binding<String>
...
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
let metaDataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
if metaDataObj.stringValue != nil {
...
data.wrappedValue = metadataObj.stringValue
...
}
}
I'm trying to integrate a UISegmentedControl into my view, which works fine. But I want to update the SwiftUI ContentView when the control's index changes.
For this purpose I want to create a Coordinator for accessing the ContentView's State through a Binding.
I followed the official
WWDC video
(relevant from min 13:37), but it throws out an error saying "Cannot assign to property: '$textString' is immutable" when I try to implement the init function of the Coordinator class.
import SwiftUI
import UIKit
struct SegControl: UIViewRepresentable {
class Coordinator: NSObject {
#Binding var textString: String
init(textString: Binding<String>) {
//Error: Cannot assign to property: '$textString' is immutable
$textString = textString
}
#objc func testFunc() {
textString = "Updated!"
}
}
#Binding var textString: String
func makeCoordinator() -> Coordinator {
return Coordinator(textString: $textString)
}
func makeUIView(context: Context) -> UISegmentedControl {
return UISegmentedControl(items: ["One", "Two"])
}
func updateUIView(_ uiView: UISegmentedControl, context: Context) {
uiView.center = uiView.center
uiView.selectedSegmentIndex = 0
uiView.addTarget(context.coordinator, action: #selector(Coordinator.testFunc), for: .valueChanged)
}
}