I am trying to use that the ARKit imageDetection functionality in a swiftUI project and have troubles to implement the renderer. This is what happened so far:
In Xcode 11.2 one can start a new ARKit project using swiftUI. The UIViewRepresentable protocol is used in the ARViewContainer struct that returns an ARView. An ARView object/var is created inside that struct and this "arView" apparently does have a "session" vobject.
I think I could set up this (AR)-session object like it used to work with SceneKit:
struct ScanARViewContainer: UIViewRepresentable {
makeUIView(context: Context) -> ARView {
//let arView = MyARView(frame: .zero)
// changed this line to the following to have an own renderer
let arView = ARView(frame: .zero, cameraMode: ARView.CameraMode.ar, automaticallyConfigureSession: false)
guard let referenceImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
fatalError("Missing expected asset catalog resources.")
}
let configuration = ARWorldTrackingConfiguration()
configuration.detectionImages = referenceImages
arView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
return arView
}
The code is compiled with no complains and when sending this to the phone the AR session is started and seems to do something.
The next step would be to change the renderer to show detected images. In SceneKit one needed to make use of the ARSCNViewDelegate (Image detection results). This is where I got stuck.
I tried to create an own myARView class first to get access to the ARSessionDelegate hoping for being able to access didadd anchor functions.
class MyARView : ARView, ARSessionDelegate {
required init(frame: CGRect) {
super.init(frame: frame, cameraMode: ARView.CameraMode.ar, automaticallyConfigureSession: false)
self.session.delegate = self
}
}
Then I wanted to uses this "new" class in the ARViewContainer struct:
let arView = MyARView(frame: .zero, cameraMode: ARView.CameraMode.ar, automaticallyConfigureSession: false)
//old : let arView = ARView(frame: .zero, cameraMode: ARView.CameraMode.ar, automaticallyConfigureSession: false)
But the compiler complains about "Type 'ARViewContainer' does not conform to protocol 'UIViewRepresentable'.
Or I get this complain "Type of expression is ambiguous without more context" when declaring the
let arView = MyARView(...'
Does anybody know how to do this correctly?
I think I found good inspiration here about how to "catch" the delegate callbacks.
ARKit & Reality composer - how to Anchor scene using image coordinates
Thanks to Mark D
Related
I want to test a ViewController hierarchy without using XCUITests, because they take a huge ton longer than plain old XCTests.
I've had some success but I can't seem to create a ViewController hierarchy more than one level deep.
Here's a test case demonstrating the issue:
class VCHierarchyTests: XCTestCase {
func test_CanMakeViewControllerHierarchy() {
let baseVC = UIViewController()
let vcToPresent = UIViewController()
let vcOnTopOfThat = UIViewController()
let vcOnTopOfThatOnTopOfThat = UIViewController()
newKeyWindowWith(root: baseVC)
baseVC.present(vcToPresent, animated: false)
vcToPresent.present(vcOnTopOfThat, animated: false)
vcOnTopOfThat.present(vcOnTopOfThatOnTopOfThat, animated: false)
//this passes:
XCTAssertNotNil(baseVC.presentedViewController)
//this fails:
XCTAssertNotNil(vcToPresent.presentedViewController)
//this fails:
XCTAssertNotNil(vcOnTopOfThat.presentedViewController)
}
private func newKeyWindowWith(root viewController: UIViewController){
let window = UIWindow(frame: UIScreen.main.bounds)
window.makeKeyAndVisible()
window.rootViewController = viewController
}
}
Is there a way to construct ViewController hierarchies without XCUITest?
Try PresentationVerifier from ViewControllerPresentationSpy to unit test how view controllers are presented.
It's designed to intercept a single present() call, not several of them. If your use case falls outside what it does, please file an Issue.
I’m using the following code that gets triggered via a button to display the share sheet in my app:
func shareSheet() {
guard let urlShare = URL(string: "https://google.com") else { return }
let activityVC = UIActivityViewController(activityItems: [urlShare], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(activityVC, animated: true, completion: nil)
}
This code brings up the warning:
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead
I don't understand how I can get rid of it. Already checked the approach suggested here How to get rid of message " 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead" with AdMob banner? but it's not working. Any ideas how I can bypass the warning?
Many Thanks!
You can use following as UIApplication.shared.currentUIWindow()?.rootViewController
public extension UIApplication {
func currentUIWindow() -> UIWindow? {
let connectedScenes = UIApplication.shared.connectedScenes
.filter({
$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
let window = connectedScenes.first?
.windows
.first { $0.isKeyWindow }
return window
}
}
Multiline text input is currently not natively supported in SwiftUI (hopefully this feature is added soon!) so I've been trying to use the combine framework to implement a UITextView from UIKit which does support multiline input, however i've been having mixed results.
This is the code i've created to make the Text view:
struct MultilineTextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
view.backgroundColor = UIColor.white
view.textColor = UIColor.black
view.font = UIFont.systemFont(ofSize: 17)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
func frame(numLines: CGFloat) -> some View {
let height = UIFont.systemFont(ofSize: 17).lineHeight * numLines
return self.frame(height: height)
}
func makeCoordinator() -> MultilineTextView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: MultilineTextView
init(_ parent: MultilineTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}
I've then implemented it in a swiftUI view like:
MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)//.frame(numLines: 4)
And bound it to a state variable:
#State var currentItem:Item
It sort of works. However, the state var currentItem:Item contains an array of strings which I'm then cycling through using buttons which update the string array based on what has been inputted into MultilineTextView. This is where i'm encountering a problem where the MultilineTextView seems to bind to only the first string item in the array, and then it won't change. When I use swiftUI's native TextField view this functionality works fine and I can cycle through the string array and update it by inputting text into the TextField.
I think I must be missing something in the MultilineTextView struct to allow this functionality. Any pointers are gratefully received.
Update: Added model structs
struct Item: Identifiable, Codable {
let id = UUID()
var completed = false
var pairArray:[TextPair]
}
struct TextPair: Identifiable, Codable {
let id = UUID()
var textOne:String
var textTwo:String
}
Edit:
So I've done some more digging and I've found what I think is the problem. When the textViewDidChange of the UITextView is triggered, it does send the updated text which I can see in the console. The strange thing is that the updateUIView function then also gets triggered and it updates the UITextView's text with what was in the binding var before the update was sent via textViewDidChange. The result is that the UITextview just refuses to change when you type into it. The strange thing is that it works for the first String in the array, but when the item is changed it won't work anymore.
It appears that SwiftUI creates two variants of UIViewRepresentable, for each binding, but does not switch them when state, ie title is switched... probably due to defect, worth submitting to Apple.
I've found worked workaround (tested with Xcode 11.2 / iOS 13.2), use instead explicitly different views as below
if title {
MultilineTextView(text: $currentItem.titleEnglish)
} else {
MultilineTextView(text: $currentItem.pairArray[currentPair].textOne)
}
So I figured out the problem in the end, the reason why it wasn't updating was because I was passing in a string which was located with TWO state variables. You can see that in the following line, currentItem is one state variable, but currentPair is another state variable that provides an index number to locate a string. The latter was not being updated because it wasn't also being passed into the multiline text view via a binding.
MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)
I thought initially that passing in one would be fine and the parent view would handle the other one but this turns out not to be the case. I solved my problem by making two binding variables so I could locate the string that I wanted in a dynamic way. Sounds stupid now but I couldn't see it at the time.
The video I'm playing does not take the entire area of the UIView (named videoView), which has a gray color: iPhone 7 Plus Simulator Screenshot
Most of the answers claim that I need to either set the frame to bounds (of UIView) or set videoGravity to AVLayerVideoGravityResizeAspectFill. I've tried both, but for some reason it still does not fill the space entirely.
var avPlayer: AVPlayer!
var avPlayerLayer: AVPlayerLayer!
var paused: Bool = false
#IBOutlet weak var videoView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let theURL = Bundle.main.url(forResource:"HOTDOG", withExtension: "mp4")
avPlayer = AVPlayer(url: theURL!)
avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
avPlayerLayer.frame = videoView.layer.bounds
videoView.layer.insertSublayer(avPlayerLayer, at: 0)
}
Any help will be appreciated. :)
After long time I found the answer.
Code below should be moved into viewDidAppear() like:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Resizing the frame
avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
avPlayerLayer.frame = videoView.layer.bounds
videoView.layer.insertSublayer(avPlayerLayer, at: 0)
avPlayer.play()
paused = false
}
The layout was designed for iPhone SE (small screen), so when it was tested on a bigger screen the app was taking originally set size from the Auto-layout and shaping it according to that. By moving the code into viewDidAppear() the app resizes the window according to new constraints.
Just move the frame line avPlayerLayer.frame = videoView.layer.bounds into viewDidLayoutSubviews like this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
avPlayerLayer.frame = videoView.layer.bounds
}
The rest should stick into the viewDidLoad function, just like you did.
I've read the documentation, gone through their wonderful Playground example, searched S.O., and reached the extent of my google-fu, but I cannot for the life of me wrap my head around how to use ReactiveSwift.
Given the following....
class SomeModel {
var mapType: MKMapType = .standard
var selectedAnnotation: MKAnnotation?
var annotations = [MKAnnotation]()
var enableRouteButton = false
// The rest of the implementation...
}
class SomeViewController: UIViewController {
let model: SomeModel
let mapView = MKMapView(frame: .zero) // It's position is set elsewhere
#IBOutlet var routeButton: UIBarButtonItem?
init(model: SomeModel) {
self.model = model
super.init(nibName: nil, bundle: nil)
}
// The rest of the implementation...
}
....how can I use ReactiveSwift to initialize SomeViewController with the values from SomeModel, then update SomeViewController whenever the values in SomeModel change?
I've never used reactive anything before, but everything I read leads me to believe this should be possible. It is making me crazy.
I realize there is much more to ReactiveSwift than what I'm trying to achieve in this example, but if someone could please use it to help me get started, I would greatly appreciate it. I'm hoping once I get this part, the rest will just "click".
First you'll want to use MutableProperty instead of plain types in your Model. This way, you can observe changes to them.
class Model {
let mapType = MutableProperty<MKMapType>(.standard)
let selectedAnnotation = MutableProperty<MKAnnotation?>(nil)
let annotations = MutableProperty<[MKAnnotation]>([])
let enableRouteButton = MutableProperty<Bool>(false)
}
In your ViewController, you can then bind those and observe those however necessary:
class SomeViewController: UIViewController {
let viewModel: Model
let mapView = MKMapView(frame: .zero) // It's position is set elsewhere
#IBOutlet var routeButton: UIBarButtonItem!
init(viewModel: Model) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
routeButton.reactive.isEnabled <~ viewModel.enableRouteButton
viewModel.mapType.producer.startWithValues { [weak self] mapType in
// Process new map type
}
// Rest of bindings
}
// The rest of the implementation...
}
Note that MutableProperty has both, a .signal as well as a .signalProducer.
If you immediately need the current value of a MutableProperty (e.g. for initial setup), use .signalProducer which immediately sends an event with the current value as well as any changes.
If you only need to react to future changes, use .signal which will only send events for future changes.
Reactive Cocoa 5.0 will add UIKit bindings which you can use to directly bind UI elements to your reactive layer like done with routeButton in the example.