Hello,
I'm having a blast learning SwiftUI but as a designer, and brand new developer, I'm really struggling with linking things and I don't quite have the vocabulary to follow Apple's documentation. With SwiftUI, I'd just love to have a button middle of the screen--I tap it and an AR Quick Look USDZ pops up.
Could somebody point me in the right direction, connecting Quick Look to a button? And pretend like you're talking to a baby?
https://developer.apple.com/documentation/arkit/previewing_a_model_with_ar_quick_look
Thank you!
-Dan
So what you describe is initially easy to put together with SwiftUI. First you need a Button, so use this for your app's ContentView:
struct ContentView: View {
var body: some View {
Button("Show Preview") {
// Action that runs when the button is tapped goes here...
}
}
}
SwiftUI will automatically center that button on the screen. When you tap it you want to show another view, and a simple way to do that is with what's called a modal view. Modal here means that it takes over the screen, so you can now only interact with this new view until you dismiss it, while the original view stays in the background. In SwiftUI, modal views are presented as sheets—that is, they slide up from the bottom of the screen to cover the original view, leaving a little space at the top so you can see that it's covering another view.
To display a sheet in SwiftUI, you need two things: first, you need a property that defines a piece of state from which SwiftUI can derive your views. This state is just a boolean value: true or false. If it's true, the sheet is displayed, and if not, it's dismissed. Secondly, you apply a modifier to your view—something that changes your view's output—to tell SwiftUI what you want displayed, and which state property to use with it. You then tie it all into the button by changing the button's action to toggle that state variable. When it's off, tapping the button will turn it on, displaying the sheet. When the sheet is dismissed, the state will be set to false, ready for your button to change it again.
Update your content view like so:
struct ContentView: View {
// State used to toggle showing our sheet containing AR QL preview
@State var showingPreview = false
var body: some View {
VStack {
Button("Show Preview") {
// Action that runs when the button is tapped.
// This one toggles the showing-preview state.
self.showingPreview.toggle()
}
// This modifier tells SwiftUI to present a view in a modal sheet
// when a state variable is set to true, and to dismiss it
// when set to false.
.sheet(isPresented: $showingPreview) {
// Sheet content here...
}
}
}
}
The view now contains one property, named 'showingPreview', set to false. It has the '@State' attribute, which tells SwiftUI that this is part of the view's state information. SwiftUI now watches it closely, and any time it's changed, your view will be updated.
I've also wrapped the button inside a VStack view. It's not changing much now, but we'll add something else in a moment. A VStack is a view that contains other views, such as your Button, and places them onscreen in order from top to bottom, one above the next.
The .sheet(isPresented:content:) function takes a binding to the state property as its first parameter. The special $-prefix syntax is used in SwiftUI to generate bindings from state. A binding is a reference back to another property—in this case, your 'showingPreview' property—that can be used to both read and write that property's value. By handing in a binding rather than just the plain property you enable SwiftUI to set the state to false when the sheet is dismissed.
Your sheet will need a couple of things itself: it needs its quick-look content view, and it would also be nice to have a visible 'close' button to dismiss the sheet (in case the user doesn't know about the built-in swipe-to-dismiss gesture). You'll implement this using a VStack and an HStack. Your VStack will contain the HStack at the top, then under that will be the quick-look view. Your HStack (which lays out leading-to-trailing) will contain the Close button.
Here's the implementation:
.sheet(isPresented: $showingPreview) {
// Sheet content: the quick look view with a header bar containing
// a simple 'close' button that closes the sheet.
VStack {
// Top row: button, aligned left
HStack {
Button("Close") {
// Toggle the preview display off again.
// Swiping down (the system gesture to dismiss a sheet)
// will cause SwiftUI to toggle our state property as well.
self.showingPreview.toggle()
}
// The spacer fills the space following the button, in effect
// pushing the button to the leading edge of the view.
Spacer()
}
.padding()
// Quick-look view goes here
}
}
The 'close' button is doing the exact same thing as the 'show preview' button: it's toggling the state property that controls the sheet. If the 'close' button is visible, then the sheet is being shown, and toggling the property will hide it.
Your quick-look view requires a little more work, though, because it's not something that SwiftUI provides directly. Instead, you need to create a new SwiftUI view that will wrap a QLPreviewController, which is what implements QuickLook preview. This is designed to work with the UIKit framework, so you'll create a different type of view to manage it.
Create a new SwiftUI file, and name it 'ARQuickLook.swift'. Use the following content:
import SwiftUI
import QuickLook
import ARKit
struct ARQuickLookView: UIViewControllerRepresentable {
// Properties: the file name (without extension), and whether we'll let
// the user scale the preview content.
var name: String
var allowScaling: Bool = true
func makeCoordinator() -> ARQuickLookView.Coordinator {
// The coordinator object implements the mechanics of dealing with
// the live UIKit view controller.
Coordinator(self)
}
func makeUIViewController(context: Context) -> QLPreviewController {
// Create the preview controller, and assign our Coordinator class
// as its data source.
let controller = QLPreviewController()
controller.dataSource = context.coordinator
return controller
}
func updateUIViewController(_ controller: QLPreviewController,
context: Context) {
// nothing to do here
}
class Coordinator: NSObject, QLPreviewControllerDataSource {
let parent: ARQuickLookView
private lazy var fileURL: URL = Bundle.main.url(forResource: parent.name,
withExtension: "reality")!
init(_ parent: ARQuickLookView) {
self.parent = parent
super.init()
}
// The QLPreviewController asks its delegate how many items it has:
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
// For each item (see method above), the QLPreviewController asks for
// a QLPreviewItem instance describing that item:
func previewController(
_ controller: QLPreviewController,
previewItemAt index: Int
) -> QLPreviewItem {
guard let fileURL = Bundle.main.url(forResource: parent.name, withExtension: "usdz") else {
fatalError("Unable to load \(parent.name).reality from main bundle")
}
let item = ARQuickLookPreviewItem(fileAt: fileURL)
item.allowsContentScaling = parent.allowScaling
return item
}
}
}
struct ARQuickLookView_Previews: PreviewProvider {
static var previews: some View {
ARQuickLookView(name: "MyScene")
}
}
There's a fair bit going on in here. First, you're creating a type of UIViewControllerRepresentable, which is a special View subtype. Instead of defining a 'body' as you did in your ContentView, here you provide a few methods that will be used to create a UIKit view controller, and then to updated that view controller as necessary. SwiftUI will handle storage for you.
Now, to display a QuickLook preview, you used a QLPreviewController. That controller makes use of a common pattern in iOS development: a data source. This is another object that it will query to find out what data it should display. In order to provide a data source, you've defined the Coordinator class inside your ARQuickLookView. This is also managed by SwiftUI on your behalf, and is made available through the special Context passed into the various methods you see above. You create an instance of this coordinator in the makeCoordinator() method; your coordinator takes a copy of the ARQuickLookView in order to refer back to its 'name' and 'allowScaling' properties.
In makeUIViewController(context:), you create your QLPreviewController, then fetch your coordinator from the context and assign it as the preview controller's data source. In updateViewController(_:context:), you don't need to do anything, but the method must be implemented or your app won't build.
Inside the coordinator you implement the two required QLPreviewControllerDataSource methods: in numberOfPreviewItems(in:), you return a count of the items it'll display. Here you're only ever doing one, so you return 1.
In previewController(_:previewItemAt:), you return the requested preview item. Here you ask your application bundle to find the named item (for 'name' it would search for 'name.usdz'). If that fails, you print an error and exit—that's what the fatalError() call does. Otherwise, you use it to create an ARQuickLookPreviewItem, and then set the 'allowsContentScaling' property on that to match the value in the ARQuickLookView. You return that item, and the QLPreviewController will do what it takes to make it appear onscreen.
At this point, you need to go back and add one last line to your ContentView, ad the end of the '.sheet' modifier. Replace the comment about 'Quick-look view goes here' with a call to create the view itself, replacing 'MyScene' with the name of the ARKit file you added to your application:
// The actual quick-look view, which will scale to fill the
// available space.
ARQuickLookView(name: "MyScene")
Your app ought to work now. However, as an exercise, can you work out how to let the user change the 'allow scaling' property of the preview? You'll need a state property and a Toggle view. Have a look through the framework and see if you can figure it out. When you're done, scroll down to see my implementation, and see how close you got.
.
.
.
.
.
.
.
.
.
.
.
.
struct ContentView: View {
// State used to toggle showing our sheet containing AR QL preview
@State var showingPreview = false
// Turns off & on the ability to change the preview size within ARKit.
@State var allowsScaling = false
var body: some View {
VStack {
// Allow the user to enable/disable scaling of the preview content.
Toggle("Allow Scaling", isOn: $allowsScaling)
Button("Show Preview") {
// Action that runs when the button is tapped.
// This one toggles the showing-preview state.
self.showingPreview.toggle()
}
// This modifier tells SwiftUI to present a view in a modal sheet
// when a state variable is set to true, and to dismiss it
// when set to false.
.sheet(isPresented: $showingPreview) {
// Sheet content: the quick look view with a header bar containing
// a simple 'close' button that closes the sheet.
VStack {
// Top row: button, aligned left
HStack {
Button("Close") {
// Toggle the preview display off again.
// Swiping down (the system gesture to dismiss a sheet)
// will cause SwiftUI to toggle our state property as well.
self.showingPreview.toggle()
}
// The spacer fills the space following the button, in effect
// pushing the button to the leading edge of the view.
Spacer()
}
// Leave a little space all around the button.
.padding()
// The actual quick-look view, which will scale to fill the
// available space.
ARQuickLookView(name: "MyScene", allowScaling: self.allowsScaling)
}
}
}
}
}