That UIDatePicker code looks suspiciously like it's accessing private state. Certainly `textColor` and `highlightsToday` aren't listed in the public API. Given that, it's unlikely you'll see those exposed by SwiftUI—private API will stay private.Also, while `setValue(_:forKey:)` won't crash, it may simply stop working at some point in the future, silently failing. Apple may even mark the version that takes a plain String for the key argument as unavailable from Swift. Such facilities can be useful, but you take on the burden of manually confirming the behavior remains the same on every single point release, because it could go away (or gain/lose side-effects) at any time.
Post
Replies
Boosts
Views
Activity
There are two parts. First, you want to make your enum conform to CaseIterable so you can iterate its cases in a ForEach view. Then, when returning the view for each picker option, use the .tag(_:) view modifier to attach the enum value itself to the picker row. SwiftUI will use this to map from views to enum case values.For localization, it's useful to have a property on your enum that will return a LocalizedStringKey. APIs like Text.init() that take a StringProtocol type actually won't do localization lookups. Those happen only for LocalizedStringKey values, which are ExpressibleByStringInterpolation, meaning that Text("Hello \(name)!") will become localized. If you pass a String variable though, it'll go through the StringProtocol initializer. Thus it's useful to explicitly create one, and to get the long name of LocalizedStringKey out of the way of your view code by hiding it in a property on the type you're describing.Here's a quick example:enum Sample: String, Equatable, CaseIterable {
case first = "First"
case second = "Second"
case third = "Third"
var localizedName: LocalizedStringKey { LocalizedStringKey(rawValue) }
}
struct ContentView: View {
@State var selection: Sample = .first
var body: some View {
Form {
Picker("This Title Is Localized", selection: $selection) {
ForEach(Sample.allCases, id: \.id) { value in
Text(value.localizedName)
.tag(value)
}
}
}
.padding()
}
}Note that things get more sticky if your enum has payloads, i.e. case something(String, Int). I'm not sure if those even can be CaseIterable, but beyond that there's the question of how to present a nested sequence of inputs. The answer is going to be highly dependant on your needs, though. Suffice to say that it is possible, but that you may need to create some reasonably complex behaviors to get everything working just right. Combine is your friend here, along with Binding.init(get:set:).
Interesting; can you share a code sample? In my version everything seemed to line up happily with the following code:struct ContentView: View {
@State var pickerChoice: Int = 1
@State var count: Int = 1
var body: some View {
Form {
Picker("Format:", selection: $pickerChoice) {
ForEach(1..<6) {
Text("\($0)")
}
}
StepperField(title: "Count:", value: $count, alignToControl: true)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Ultimately there are a few things you'll need to do.Issues 1 & 3 are caused by the same thing: the AppKit view's size isn't being set up cleanly. You'll need to manually create constraints to ensure it fills its allotted area, or else take the simpler approach of using the autoresizing mask. In the latter case, you'll need to turn off translatesAutoresizingMaskIntoConstraints as well. Do this in makeNSView(context:) and you'll be good on that score. NSViewRepresentable views will automatically claim all the space they're offered, so this is all you should need to do.Issue 2 requires you to implement a Coordinator that conforms to the NSTextViewDelegate protocol. Give it a reference to the owning view, and implement textDidChange(_:) to pass on the new text value via the binding in the main view.Here's a sample of my implementation:public struct TextView: View {
private typealias _PlatformView = _AppKitTextView
private let platform: _PlatformView
public init(text: Binding<String>) {
self.platform = _PlatformView(text: text)
}
public var body: some View { platform }
}
fileprivate struct _AppKitTextView: NSViewRepresentable {
@Binding var text: String
func makeNSView(context: Context) -> NSTextView {
let view = NSTextView()
view.delegate = context.coordinator
view.textColor = .controlTextColor
view.translatesAutoresizingMaskIntoConstraints = false
view.autoresizingMask = [.width, .height]
return view
}
func updateNSView(_ view: NSTextView, context: Context) {
view.string = text
if let lineLimit = context.environment.lineLimit {
view.textContainer?.maximumNumberOfLines = lineLimit
}
view.alignment = _alignment(from: context.environment)
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, NSTextViewDelegate {
var parent: _AppKitTextView
init(_ parent: _AppKitTextView) {
self.parent = parent
}
func textDidChange(_ notification: Notification) {
guard let text = notification.object as? NSText else { return }
self.parent.text = text.string
}
}
}There's a more fully-featured implementation for both macOS and iOS available as part of my AQUI library on Github.
Oof. This went down a rabbit hole and no mistake. Happily though, it turns out to be fairly simple to do what we need.Conceptually, the StepperField view looks like this:HStack {
Title
TextField
Stepper
}What we want is to get the value of the TextField's leading alignment guide and use that value to set the leading alignment guide of the surrounding HStack. Pushing that value upwards is straightforward, but you can't use the standard alignment values to do so, as they already have settings. If you create your own custom alignment, then a sub-view can set a guide value for that and it'll be inherited by its parent. Thus we want to do:HStack {
Title
TextField(...).alignmentGuide(.someValue) { $0[.leading] }
Stepper
}
.alignmentGuide(.leading) { $0[.someValue] }Here you get the value of the TextField's leading alignment and store it a new alignment value. Then in the HStack you read that value and assign it to the HStack's leading alignment, which is what the Form view is using to align its content. I tried using .leading in both cases, but that value didn't get passed up it seems—at the HStack level, the custom alignment is available as an expicit value, but the same isn't true of any leading alignment set by the TextField.So, you need to define a custom alignment. This one seems like it'll be useful, so it would make sense to put it somewhere easily accessible to other controls:extension HorizontalAlignment {
private enum ControlAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
return context[HorizontalAlignment.center]
}
}
static let controlAlignment = HorizontalAlignment(ControlAlignment.self)
}Now you can use this to pass the text field's leading offset up the stack. A useful side-effect of this approach is that, with explicit alignments in play, you no longer need a trailing Spacer on the StepperField:struct StepperField: View {
let title: LocalizedStringKey
@Binding var value: Int
var body: some View {
HStack {
Text(title)
TextField("Enter Value", value: $value, formatter: NumberFormatter())
.multilineTextAlignment(.center)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(minWidth: 15, maxWidth: 60)
.alignmentGuide(.controlAlignment) { $0[.leading] }
Stepper(title, value: $value, in: 1...100)
.labelsHidden()
}
.alignmentGuide(.leading) { $0[.controlAlignment] }
}
}Everything will now work happily, and the Stepper field will now align itself correctly within the form.However, if you swap your Form for a List, or for a VStack with leading alignment, you'll see the stepper slide too far to the left. Only the Form is using this special behavior. I'd guess that internally there's a special 'control alignment' value that the Form is ultimately using, and its default is '.leading'. That would explain why setting our leading alignment lets us line up our controls correctly. Unfortunately we don't have a way of easily determining whether we're in a List, Form, etc. I don't see anything inside EnvironmentValues that we could use to automatically infer our context, though, so likely the only real option from out here is to have a custom value set in the StepperField initializer that tells it where to align:struct StepperField: View {
let title: LocalizedStringKey
@Binding var value: Int
var alignToControl: Bool = false
var body: some View {
HStack {
Text(title)
TextField("Enter Value", value: $value, formatter: NumberFormatter())
.multilineTextAlignment(.center)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(minWidth: 15, maxWidth: 60)
.alignmentGuide(.controlAlignment) { $0[.leading] }
Stepper(title, value: $value, in: 1...100)
.labelsHidden()
}
.alignmentGuide(.leading) {
self.alignToControl
? $0[.controlAlignment]
: $0[.leading]
}
}
}
Ultimately it'll all come down to the use of anchors and alignment values, as that's what SwiftUI will be using to line things up. Generally, though, it's lining up the labels of the controls, so I'm not sure what value to use for that. I would guess it's using a custom 'leading' alignment that's set to the leading edge of the actual control rather than its label, but I'm not certain.I'll have a dig around and see what I can come up with.
You need to mark the SwiftUI framework as being imported weakly. The normal way to include frameworks in Swift apps is to do nothing—the compiler uses the import statements to determine which modules to load, and generates the linker commands to that effect. You can customize this behavior in Xcode, but you first need to explicitly link against SwiftUI, then you can mark the framework as optional.In the Xcode Project navigator, select your project file (blue Xcode icon, topmost in the tree).In the editor that opens, ensure your app target is selected (or whichever target you're using SwiftUI from).Select the “Build Phases” tab.Pop open the “Link Binary With Libraries” section.Click the + button under the list that appears.Type “SwiftUI” into the search field of the popup sheet, select “SwiftUI.framework,” and click “Add.”On the new row in the table, click the “Required” popup button in the last column.Change the value to “Optional.”Build your app/framework now, and if you look at the logs for the link step you'll see -weak_framework SwiftUI in the arguments. This is what tells the linker that the SwiftUI framework is optional, and it shouldn't crash if it's not available (all imported symbols will be zero, and will now cause a crash only if used).If you feel like going the extra mile to check on things, you can use otool -lvV to check the output binary, where you'll see something like this:Load command 13
cmd LC_LOAD_WEAK_DYLIB
cmdsize 80
name /System/Library/Frameworks/SwiftUI.framework/SwiftUI (offset 24)
time stamp 2 Wed Dec 31 16:00:02 1969
current version 1.0.0
compatibility version 1.0.0
Load command 14
cmd LC_LOAD_DYLIB
cmdsize 88
name /System/Library/Frameworks/Foundation.framework/Foundation (offset 24)
time stamp 2 Wed Dec 31 16:00:02 1969
current version 1673.126.0
compatibility version 300.0.0Note that SwiftUI uses LC_LOAD_WEAK_DYLIB while Foundation uses LC_LOAD_DYLIB. This is what we want.Now, I don't have any iOS 12 devices or simulators around to check this myself, but this ought to be everything you need to get things up & running—just make sure you're properly wrapping all your SwiftUI code in @available blocks, as you've already started to do.
Can you attach a stack trace here? It would be interesting to know what component is raising the error, and that may help nail down a culprit.
I'll have to dig some more into this to get proper answers for you, but using `init()` is absolutely the right way to do this. FetchRequest is a DynamicProperty, so it implements a method named `update()` which is called before rendering an associated view `body`. Its initializer just copies the `NSFetchRequest` and `Transaction` (if supplied), arguments, then all the real work—including resource allocation—happens either in `update()` or when you access the `wrappedValue`.I did some spelunking into how it was implemented and wrote a very slightly modified version which allows me to swap out the NSFetchRequest at will. The code is in my AQUI project on Github.
TextField is actually fairly flexible. By default you'd bind it to a String, true, but you can bind it to any type you like, so long as you provide a Formatter for it to use. That'll then let you bind both the stepper and the text field to the same underlying value. Additionally, if you wanted to merely show the value via some static text, then you'd be able to simply access the Int value directly in a string interpolation.Here's a working sample that does what I believe you want:struct ContentView: View {
@State var value: Int = 0
var body: some View {
VStack(alignment: .leading) {
Text("Enter Value (\(value))")
.font(.headline)
HStack {
TextField("Enter Value", value: $value, formatter: NumberFormatter())
.multilineTextAlignment(.center)
.keyboardType(.decimalPad)
.frame(minWidth: 15, maxWidth: 60)
.textFieldStyle(RoundedBorderTextFieldStyle())
Stepper("Value", value: $value, in: 0...100)
.labelsHidden()
Spacer()
}
}
.padding()
}
}
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)
}
}
}
}
}