Ask Apple 2022 Q&A Related to SwiftUI (Part 1)
Ask Apple provides an opportunity for developers to directly communicate with Apple engineers outside of WWDC. This article compiles some of the Q&A related to SwiftUI from the event, and includes some personal insights. This is Part 1 of the article.
Q&A
UIActivityViewController
Q: Is there any plan to add “native” SwiftUI support to the UIActivityViewController for iOS?
A: It is already available! Please check out ShareLink.
contextAction
Q: In the early iOS 16 and macOS 13 beta versions, we saw a new .contextAction
modifier, which was later removed. Is there any suggestion for detecting row selection in a list, similar to “NavigationLink”, but without navigating to another view (e.g., showing a sheet or selecting an option from the list)? A button might work for iOS and iPadOS, but not so much for macOS. Also, .contextAction
supports multiple selection. Will it come back?
A: Take a look at the primaryAction parameter of the contextMenu modifier. This API also has a forSelectionType parameter that supports multiple selection.
In SwiftUI 4.0, the contextMenu modifier has been significantly improved. For example, a context menu can have multiple options, support primaryAction, and have a customizable preview view. The contextMenu with the primaryAction parameter mentioned above can not only be used for List but also for Table.
How to test @State variables
Q: Is there a recommended way to test @State variables in SwiftUI views? Is the only way to refactor these variables into a view model?
A: If there are multiple interrelated @State properties in the same view, extracting them into a structure might be a good choice. Extracting them into a view model is also a strategy, but not necessary.
It is difficult to test dependencies (conforming to the DynamicProperty protocol) in SwiftUI views during unit testing. This is also one of the advantages of Redux-like frameworks (extracting state from views for easy testing). Please read the article ”Writing testable code when using SwiftUI” to learn how to write test-friendly view code.
Creating a bottom text input bar similar to an IM application
Q: Hello, my question is about TextField. Suppose we want to create a view similar to iMessage, where you can see a list of messages (irrelevant to this example) and at the bottom of the view there is a text field. When the user clicks on the text field, the keyboard appears in its toolbar. I tried adding a TextField in ToolbarItemGroup(place: .bottomBar), a second one in ToolbarItemGroup(place: .keyboard), and then, with the help of the @FocusState variable, I can hide one and move the focus to the keyboard. This is a bit cumbersome, and I don’t think having two text fields is the right approach. Also, with this approach, the @FocusState variable becomes unresponsive and it cannot be set to nil (the keyboard is not removed when returning to the previous view). Is it possible to achieve this in pure SwiftUI (without using UIKit)? Can you give me some direction to accomplish it?
A: Generally, I recommend using .safeAreaInset(edge: .bottom)
to implement the bottom text field. Then customize its display style based on its focus state. I hope this is useful for your design.
Since the release of safeAreaInset view modifier in SwiftUI 3.0, implementing the case in the problem will no longer be difficult. For more information, please refer to the article Mastering Safe Area in SwiftUI.
How to avoid recomputing the view that creates an instance when using environmentObject
Q: How can I share state (such as ObservableObject) between two sibling views in different sub-trees without recomputing the top-level view body? For example, I can have a StateObject in the parent view and pass that object through EnvironmentObject. However, if the @Published property inside it changes, both the parent view and its sub-tree are recomputed.
A: EnvironmentObject is a great tool. If you don’t want the parent view to be updated, you can create the object without using @StateObject or @ObservedObject.
Note that if there is a need to modify the environment object instance in the parent view, it should be ensured that the parent view is not repeatedly reconstructed (SwiftUI recreates instances of view types). For details, please refer to SwiftUI’s StateObject and ObservedObject: The Key Differences as advised by Apple engineers.
NavigationPath
Q: I’m glad to see the new NavigationStack/NavigationPath, they work well for me. I’m wondering if I can inspect the NavigationPath to determine if my SearchView has entered a certain view (just as an example). I already have an idea of using NavigationPath.CodableRepresentation, but I’m concerned that this may not be the best or most sustainable way to observe NavigationPath. Thanks!
A: There’s no way to introspect NavigationPath. If you need to know the contents of the path, a good approach is to use a homogeneous PATH, like @State private var path: [MyEnum]
, and then toggle on the navigationDestination on that enum type.
NavigationPath creates a completely type-erased collection of data that only requires elements to conform to the Hashable protocol. NavigationPath has an interesting and powerful feature of being able to encode and decode itself to JSON in the absence of all type information of its elements. Read the article Reverse Engineering SwiftUI’s NavigationPath Codability to learn about how it’s implemented.
Locking the Y-axis Scale for Charts
Q: I have a Swift chart and I’ve implemented a RuleMark that displays while dragging by listening to drag events. During the drag, the Y-axis scale gets larger. In my example, it goes from 0 to 75 when not dragging and from 0 to 100 when dragging. Is there a way to prevent this?
A: You can use .chartYScale(domain: 0 ... 75)
to lock the Y-axis scale domain.
Implicit and Explicit Animations
Q: Hello! Is there any other way to animate views directly based on state changes without using the onChange decorator? Here is my code.
.onChange(of: modle.state.aChange { value in
withAnimation(...) {
self.isAnimated = value
}
}
)
A: You can animate specific state changes directly by using .animation(.easeInOut, value: model.state)
. Any changes to model.state
will trigger the animation.
By using the animation modifier bound to a specific state (the old version of the animation modifier has been deprecated), more precise animation effects can be achieved. Read the article ”Demystifying SwiftUI Animation: A Comprehensive Guide” to learn more about animations.
Adaptive Height Sheet
Q: How can I present a Sheet in iOS 16 that matches the height of dynamic content? I want to use the view’s height in presentationDetents.
A: Thank you for your question. This is currently not possible, but it is something we are interested in.
It is estimated that Apple’s engineers are busy and have not seriously considered this issue. In iOS 16, by combining presentationDetents with GeometryReader, a Sheet that matches the content height can be created. Here is the complete code.
ToolbarContentBuilder
Q: I would like to see better support for code like @ToolbarBuilder or if condition { ToolbarItem(a) } else { ToolbarItem(b) }
.
A: You will never see @ToolbarBuilder because @ToolbarContentBuilder has been enhanced in iOS 16. @ToolbarContentBuilder now provides support for if else
and can use dynamic properties such as @Environment / @EnvironmentObject in custom types that conform to ToolbarContent.
WindowGroup
Q:Good morning! I’m a SwiftUI beginner. My question is about scenes. In almost all tutorials and sample code repositories, only one WindowGroup scene is used, with all content nested within ContentView. Is there any guidance or examples on how to use multiple scenes? Or do most applications only need one WindowGroup?
A: Multiple scenes are useful for building complex applications, especially on macOS. For example, you may want an application that defines both a “window group” and a “document group” simultaneously, or an application that has a “window group” and an auxiliary “window” scene. The content view of a scene defines the view content in the windows created by the scene, but the scene itself defines the overall structure of the application.
In SwiftUI 4.0, WindowGroup has received significant updates, truly enabling the development of macOS applications. For more information, please read ”Building Adaptable SwiftUI Applications for Multiple Platforms” and WWDC 2022 Session.
DocumentGroup
Q:When using SwiftUI application lifecycle and DocumentGroup on macOS, if the application is only a data reader, can new file creation be disabled? A: DocumentGroup provides an initializer to create a reader-type application. It only allows opening files of that content type, but not editing them.
MVVM
Q:During the UIKit era, MVVM was a common architecture where the data for the view display came from a separate viewModel class. This still applies in SwiftUI, or is the struct itself now considered the viewModel?
A: SwiftUI attempts to be agnostic to the overall architecture of the application. However, in the traditional sense of viewModel, I do not recommend using the view (the struct itself) as the viewModel. This could lead to some undesirable consequences, such as reducing the reusability of the view and tying business logic to the lifecycle of the SwiftUI view, making handling business logic more difficult. In short, we do not recommend using the view as the viewModel. However, SwiftUI does provide tools to implement the classic MVVM architecture (such as StateObjects, ObservedObjects).
onAppear、init、viewDidLoad
Q:In my application, I host SwiftUI views in a UIHostingController, all of which are within a UITabBarController. Recently, I noticed that the onAppear of SwiftUI views is triggered at unexpected times, such as when the UITabBarController is created, rather than when the view itself appears. I’m wondering: 1. When should onAppear be called for SwiftUI views in a UITabBarController like this? 2. What is the recommended way to execute code when a view appears in a UITabBarController?
A: When using a UIHostingController in other types of UIViewControllers, you may trigger the view loading early by calling methods on the hosting controller. For non-lazy views (such as LazyVStack), onAppear will be called once the hosting controller’s view is initialized. For lazy views, the view is initialized when layoutSubviews or sizeThatFits is called on the hosting controller’s view. So, if you see the view being initialized in your UITabBarController’s init method, you need to see what exactly is being done in init. Try moving that work to viewDidLoad of the UITabBarController.
Views in lazy containers will repeatedly call onAppear and onDisappear depending on whether they appear in the visible area. However, onAppear and onDisappear are not the starting and ending points of a view’s lifespan. In fact, these views (views in lazy containers) will persist until the lazy container is destroyed. Please read Timing of onAppear Invocation for more information.
Universal Navigation Model
Q:We are using NavigationStack with path parameters, but we are having trouble with the “transition” path when the user resizes the window from Regular to Compact in the stage manager. In regular width, we have a sidebar with a navigation stack in the detail view. In compact width, we have a tab bar with each tab having a navigation stack.
A: The best approach currently is to build a navigation state model object, which holds a canonical representation of the navigation state. It can provide specialized programmatic bindings for your regular and compact displays. For example, in your model, you have multiple paths, one for each tab, but in the split view, only one path’s details are projected. Use a common underlying data source, and project it to the UI’s needs, so that the model can be unit tested to ensure consistency between regular and compact projections.
In SwiftUI 4, compact and regular correspond to two different controls: NavigationStack and NavigationSplitView. They have completely different driving modes. Developers are currently trying to create a model that can elegantly provide paths for both modes simultaneously. Read about the New Navigation System to learn about their differences.
Methods and Efficiency of Position Offset
Q: What is the best method to render circular images in a non-linear position (with 2 axes)? Currently, I am using ZStack and offsetting the images to place them where I want them, but I am not sure if this is the most efficient method.
A: As long as the performance is good enough to meet your use case, it is a viable method. For me, this seems like a completely reasonable implementation. If you encounter performance issues or want to significantly increase the number of images you are drawing, you can try using .drawingGroup and Canvas APIs, both of which can be used for denser drawing.
In SwiftUI, there are many ways to achieve offset, such as offset, alignmentGuide, padding, position, etc. In addition to personal preference, you should also consider whether the offset view will affect the surrounding views (on the layout level). For more details, please read Layout in SwiftUI Way.
Size rules for NavigationSplitView
Q:Hi there! I have started using NavigationSplitView and I really like it. In some cases, I want to make decisions based on whether the view is collapsed or not (for example, display a message in the detail view if it’s expanded, or display a warning or other indication if it’s collapsed). Can I assume that if the horizontal size class is compact, it’s collapsed? Or is there a more reliable way to determine this?
A: Compact does indeed correspond to a collapsed navigation split view.
How to improve the efficiency of a view containing a large number of UITextField
Q: I have a SwiftUI view containing 132 UITextFields. I know that this is a large number, but it is determined by the business logic. After a lot of struggle with memory leaks, I managed to make it work. However, the transition from one text field to the next doesn’t feel smooth enough, and every time I enter a letter in a text field, my CPU usage seems to skyrocket to 70% - 100%. In addition, when implementing the same functionality with UIKit, there are no performance issues.
A: If you encounter performance issues using UITextField on iOS, you can try to avoid having each view be a UITextField. Default rendering as Text, and dynamically switch to UITextField when the text is clicked.
Cross-View Hierarchical Sharing
Q: What is the best way to share data between multiple views when the data comes from API response? I’m using environmentObject as a wrapper for all views in ContentView, and I use @EnvironmentObject to access this data in each view. Is this the best approach for this situation? The only problem with this approach is that memory usage increases when I add new data.
A: @EnvironmentObject/environmentObject may be the best tool for sharing the same model across view hierarchies. Using them should create only one instance, which can be read in child views. This shouldn’t increase memory usage (if it does, please provide feedback). If you append more and more data to your model object, you may increase memory usage, which is normal. To overcome this, a technique is to save some data on external storage, keep only the most relevant data and an identifier in memory, so that the rest of the data can be fully retrieved.
task vs onAppear
Q: Is there any difference between .task
and .onAppear
for synchronous operations? In other words, if I write Color.green.task { self.someState += }
, can I guarantee that the state will change before the view appears for the first time? I ask this because I like to use .task(id:...)
instead of .onAppear
and .onChange(of:)
.
A: Both onAppear and task are called before we run the body on the view for the first time. For your use case, they are equivalent in behavior.
Read Mastering the SwiftUI task Modifier to learn more about task.
WindowGroup 和 OpenWindowAction
Q: Is it possible to attach parameters when creating a new window on macOS? I am creating a new managed object in the same sub-context and would like to send this object to a new window. Currently, my approach is to save references to the sub-context and managed object in a singleton, and then open a new window with a URL that checks for the context and object in the singleton. It would be better if we could launch a new window with custom parameters. A: In macOS Ventura, we introduced new APIs on WindowGroup that allow you to pass data to a window when it is opened. This can also be used in conjunction with OpenWindowAction. Please note that your data needs to be optional or specify a default value because in some cases, the framework itself will create windows (e.g. when selecting the New Window menu item). It can also work on iPadOS by creating a new scene, which is either a 2/3 or 1/3 split.
Initializing @StateObject in the constructor
Q: Is there a way to initialize a @StateObject with a view’s struct parameter in SwiftUI?
A: Yes, you can achieve this by manually initializing the @StateObject in the init method. self._store = StateObject(wrappedValue: Store(id: id))
. To clarify, the underscore makes it look a bit weird, but accessing the underlying storage is not wrong. The official documentation mainly tries to point out the most common use cases so that people don’t try to directly initialize their property wrappers right from the start. By the way, trying to initialize @State through the underlying storage is something we warned about in the past. Not because it doesn’t work, but because if you don’t have a deep understanding of how @State and identity work, its behavior can be quite confusing.
Property wrapper types are translated by the compiler at compile time by first looking at the user-defined code for the property wrapper type. For the meaning and usage of the underscore, see Going Beyond @Published:Empowering Custom Property Wrappers.
San Francisco Width Styles
Q: How to use the three new width styles (Compressed, Condensed, Expanded) added to the SF font family in SwiftUI?
A: You can use the fontWidth modifier to make adjustments.
Unfortunately, it only supports SF and does not have any effect on Chinese. Please read the article ”How to change SwiftUI Font Width” to learn about specific usage.
Adding Shortcuts to Stepper
Q: How can we add increment and decrement shortcuts to the SwiftUI Stepper (on MacOS)?
A: One way to achieve similar behavior is to use commands in a menu to provide the same actions. Normally, there should be a list to let people know which keyboard shortcuts are available. However, if this does not fit your use case, we would be interested in feedback on enhancements in this area.
You can achieve similar requirements by hiding a Button that includes shortcuts.
struct ContentView: View {
@State var value = 10
var body: some View {
Form {
Stepper(value: $value, in: 0...100, label: { Text("Value:\(value)") })
.background(
VStack {
Button("+") { value += value < 100 ? 1 : 0 }.keyboardShortcut("+",modifiers: [])
Button("-") { value -= value > 1 ? 1 : 0 }.keyboardShortcut("-",modifiers: [])
}.frame(width: 0).opacity(0)
)
}
}
}
LabeledContent
Q: Labels are sometimes (mistakenly) used to provide textual explanations for a value (e.g. account balance is 10 dollars), but some developers are unaware that this explanation cannot be read by VoiceOver. Besides creating a LabeledValue component, are there any other solutions provided by SwiftUI?
A: SwiftUI now has a LabeledContent view that you can use to label some content. LabeledContent comes with built-in formatting support! For example, LabeledContent("Age", value: person.age, format: .number)
.
Read Mastering LabeledContent in SwiftUI for more use cases of LabeledContent.
if Statements in ViewBuilder
Q: I know that SwiftUI is based on ResultBuilder. So if statements are operated through a tree structure and buildEither. Are there any considerations when using if statements in SwiftUI?
A: Regarding if/else, it is important to consider how they affect the identity of the views, which was discussed in a great session at WWDC.
In some cases, using lazy view modifiers not only maintains the stability of the view identity but also allows for more optimizations in SwiftUI. For example, using .opacity(value < 10 ? 1 : 0.5) instead of if value < 10 else
Initializing @State
Q: What is the correct way to set the value of a @State var during initialization? I know @State should be an internal value, but in some cases, we need to pass in a value from the outside, which doesn’t seem feasible for onAppear. The following method doesn’t always work for some reason.
init(id: UUID) {
self._store = StateObject(wrappedValue: Store(id: id))
}
The developer may have misunderstood the question and provided the same answer as for StateObject above. The asker may want to reinitialize the value of State by continuously modifying the value of the id parameter in the parent view. This involves a feature of all property wrappers that conform to the DynamicProperty protocol: only the instance initialized the first time during the view’s lifetime will be associated with the view creation. Please read How to Avoid Repeating SwiftUI View Updates for more information. Passing it through the environment value from the parent view should meet the asker’s current needs: the parent view can pass in a new value, and the current view can also change that value within the view’s scope.
Summary:
I overlooked the issue of not obtaining a conclusion. I hope the above summary can be helpful to you.
This Q&A article covers various topics related to SwiftUI discussed during the Ask Apple 2022 event. This is the second of two parts.