肘子的 Swift 记事本

Ask Apple 2022 Q&A Related to SwiftUI (Part 2)

Published on

Get weekly handpicked updates on Swift and SwiftUI!

Ask Apple 2022 Q&A Related to SwiftUI (Part 2)

Ask Apple provides developers with the opportunity to directly communicate with Apple engineers outside of WWDC. This article summarizes some of the Q&A related to SwiftUI in this event and adds some personal insights. This is the second part of the article.

Part One

Q&A

Form vs List

Q: This may be a very stupid question, but I’ve always been confused about Form and List. What is the difference between them, and when should I use Form and when should I use List? Thank you!

A: Form is a way of grouping many related controls together. Although Form and List look similar on iOS, if you look at macOS, you’ll find quite a few differences between them. Many of the controls in Form have different appearance and behavior compared to List on macOS. Unlike Form, List has built-in support for Edit Mode. Therefore, if you’re creating a view to display scrollable content and may perform selection operations, using List on iOS and macOS will provide the best experience. If you need to render many related controls, using Form will provide the best default experience on iOS and macOS.

Apart from early versions of SwiftUI, the performance and subview lifecycle of Form, List, LazyStack, and LazyGrid are quite similar. SwiftUI 4.0’s Form has changed significantly on Ventura, and is now more similar to the iOS style while also being more adaptable to macOS.

https://cdn.fatbobman.com/image-20221031081829661.png

Hiding Images in Accessibility Mode

Q: Is there any difference between Image(decorative:) and .accessibilityHidden in terms of accessibility?

A: There is no difference, both methods can be used to hide images appropriately so that they are not detected by accessibility technologies!

accessibilityHidden supports any element that conforms to the View protocol, and its hidden state can be dynamically adjusted.

Table Context Menu

Q: If I add a context menu to a TABLE, how can I determine which row triggers the display of the menu (without selecting the row)?

A: Use contextMenu(forSelectionType:) outside of the TABLE.

In the first question of the previous article, we have introduced the usage of contextMenu(forSelectionType:). Unlike the commonly used contextMenu, contextMenu(forSelectionType:) is used for the entire List or Table (not for individual cells). Read Creating Tables in SwiftUI with Table to learn the specific usage of Table.

Performance Optimization of Views

Q: What is the best practice for controlling the update scope in a view (to avoid unnecessary forwarding and duplicate computation) when dealing with complex user interfaces? In more complex UIs, performance (at least on macOS) rapidly declines due to the fast update speed of views.

A: There are different strategies.

  • ObservableObject is the unit that invalidates views or view hierarchies (triggering recomputation). You can use different objects that conform to the ObservableObject protocol to split the scope of invalidation.
  • Sometimes, it is useful to obtain some manual control and directly publish changes to objectWillChange without relying on @Published.
  • Add an intermediate view that extracts only the properties you need and relies on SwiftUI’s equality check to terminate invalid computations in advance.

The answer given by Apple engineers is consistent with many of the suggestions in the article on How to Avoid Repeating SwiftUI View Updates. Performance optimization of views is a systematic engineering task, and with a comprehensive understanding of its operating mechanism, injection principles, and update timing, more targeted solutions can be better developed.

Quickly Searching Array Elements

Q: Why is there no easy way to map selected rows of a TABLE to the array elements providing the table’s content? It seems the only way is to search for matching id values in the array, which can be inefficient for large tables.

A: Storing selections using array indexes is fragile: if the array mutates, the selection becomes unsynchronized. Swift Collections has an OrderedDictionary that may be helpful to you.

This is precisely what the Swift Identified Collections project is for. Swift Identified Collections is a keyed class array based on OrderedDictionary. The only requirement is that the elements must conform to the Identifiable protocol.

Swift
struct Todo: Identifiable {
  var description = ""
  let id: UUID
  var isComplete = false
}

class TodosViewModel: ObservableObject {
  @Published var todos: IdentifiedArrayOf<Todo> = []
  ...
}

// You can operate on elements similar to a dictionary to locate them quickly, and it is not easy to cause exceptions in ForEach when updating IdentifiedArray.
todos[id:id] = newTodo

Custom Layout

Q: How important is it to handle edge cases of very small or very large available space when implementing a custom layout?

A: Like many things, the answer to this question depends on your use case (no matter how unsatisfying that answer may be :sweat_smile:). If your container demands zero and infinite available space, and you need to determine the minimum and maximum dimensions, then at least these cases should be considered. Furthermore, you should consider it if you are trying to implement a universal layout that can be used in various situations. However, if you only use it yourself and the conditions are controllable, it is also reasonable not to handle these cases.

Creating a universal layout that considers all situations (such as VStack, HStack) is a rather difficult task. Even if developers cannot implement such a layout container, they should have a clear understanding of defining various size requirements. The article about SwiftUI Layout: The Mystery of Size introduces several recommended size patterns.

How to reduce the burden on the main thread

Q: How to avoid placing all operations on the main thread? Any variable marked with @Published should be modified on the main thread, so @MainActor should be used. However, any code that touches that property will be affected. Is there a recommended standard pattern or method to improve this?

A: Generally speaking, you do need to interact with the UI framework on the main thread. This is especially important when using reference types because you must ensure that there is always serialized reading of it. In fact, we have a great WWDC presentation that goes into detail about concurrency and SwiftUI, specifically mentioning the case of using ObservableObject. Generally, the performance bottleneck is not around writing to @Published properties. My recommended approach is to do any expensive or blocking work outside the main thread and then only jump back to the main thread when you need to write to a property on ObservableObject.

@State is thread-safe, and @StateObject automatically marks the wrappedValue (a reference type that conforms to the ObservableObject protocol) as @MainActor.

Custom Layout

Q: I often want to arrange various widgets based on the longest or shortest text in the list. Given that the size of dynamic text may change during application runtime, what is the best way to measure the text size for a given font?

A: Hi there! Our new layout protocol supports this feature. The full implementation of any custom layout is longer than what I can quickly outline here in this post, but the overall idea is that you can create a layout to query the ideal size of its children and sort them accordingly. Then, you can use a vertical or horizontal stack layout to combine it so that you don’t have to do all the implementation work yourself.

Jane’s video on Auto Layout based on width is very relevant to this issue. Read The SwiftUI Layout Protocol to learn how to create custom layouts.

Creating a Bottom-Aligned Scroll View

Q: How can I implement a scroll view that is aligned to the bottom, and will there be poor performance on macOS? I have tried the common solution of rotating the scroll view and each cell inside to achieve the desired inverted list, which works well on iOS. However, on macOS, it keeps the CPU usage at 100%.

A: Your best option is to use ScrollView and ScrollViewReader, and scroll to the bottom view on onAppear or when new content comes in. I do not recommend trying to rotate the scroll view.

Swiftcord’s code shows how to implement an inverted list in SwiftUI. Read Demystifying SwiftUI List Responsiveness: Best Practices for Large Datasets article to learn the recommended method by Apple engineers. Of the two approaches, if you have a large amount of data, I prefer the first method as it allows for on-demand data fetching.

Customizing List

Q: Is there a way to use List in a completely customizable way so that I can remove indentation, separators, and even change the background of the entire list? Currently, I always look for LazyVStack as a replacement.

A: There are several modifiers that can achieve this function: listRowSeparator, listRowInsets. The entire list filling is not supported, please provide feedback on this.

In SwiftUI 4, you can use .scrollContentBackground(.hidden) to hide the default background of the list.

searchable

Q: Is there a way to programmatically set the focus on the search field in the .searchable() modifier?

A: You can use the dismissSearch environment property to programmatically dismiss the search field. Currently, there is no API to programmatically set the focus on the search field.

TextField Content Validation

Q: How can I implement a SwiftUI TextField that only accepts numbers, including decimals?

A: Provide a FormatStyle to the text field to automatically convert the text into various numbers. However, this conversion only happens when the text field finishes editing, and it does not prevent non-numeric characters from being entered. Currently, SwiftUI does not have an API to restrict the characters that a user can input into a field.

I really hope that Apple continues to expand the FormatStyle-based solution to enable real-time content validation. Read the article ”Advanced SwiftUI TextField: Formatting and Validation” to learn about other validation methods and how to use onChange to limit input characters almost in real-time.

Extend background to safe area

Q: If I have a custom container type that can take a top and bottom view, is there a way for the API caller to extend the background of the provided views to the safe area while keeping the content (such as text or buttons) within the safe area?

A: You can try using the safeAreaInset(edge: .top) {...} or safeAreaInset(edge: .bottom) {...} modifiers to place your top and bottom views. Then make the top/bottom view ignore the safe area. I’m not sure if this will meet your use case, but it’s worth a try.

In the background modifier, you can set the ignoresSafeAreaEdges parameter to determine whether to ignore the safe area. This trick is useful for views that are at the top or bottom of the screen. See the tweet for more details.

Animated Transitions

Q: Why isn’t the animation transition displaying in the code below?

Swift
struct ContentView: View {
    @State var isPresented = false
    var body: some View {
        VStack {
            Button("Toggle") {
                isPresented.toggle()
            }
            if isPresented {
                Text("Hello world!")
                    .transition(.move(edge: .top).animation(.default))
            }
        }
    }
}

Try moving the animation decorator outside of the transition parameter.

Swift
struct ContentView: View {
    @State var isPresented = false
    var body: some View {
        VStack {
            Button("Toggle") {
                withAnimation {
                    isPresented.toggle()
                }
            }
            if isPresented {
                Text("Hello world!")
                    .transition(.move(edge: .top))
                    .animation(.default, value: isPresented)
            }
        }
    }
}

In the modification code provided by the Apple engineer above, .animation(.default, value: isPresented) is unnecessary. The transition animation event is explicitly added through withAnimation. For similar cases, you can also avoid using explicit animation drivers (without using withAnimation), simply by moving .animation(.default, value: isPresented) outside of the VStack. Read the article ”Demystifying SwiftUI Animation: A Comprehensive Guide” for more information about animations.

Using LazyVStack in the sidebar of NavigationSplitView

Q: Currently, the new NavigationSplitView in iOS 16 only works with List in the master column. This means that we cannot use LazyVStack or any other custom views that bind selection with the detailed view. Are there any plans to extend this functionality?

A: In iOS 16.1, you can place a navigationDestination in the sidebar, so that the NavigationLink in the sidebar will replace the root view of the detailed column.

Swift
NavigationSplitView {
    LazyVStack {
        NavigationLink("link", value: 213)
    }
    .navigationDestination(for: Int.self) { i in
        Text("The value is \(value)")
    }
} detail: {
    Text("Click an item")
}

This is a quite significant improvement! It solves a major regret that existed before. As a result, the flexibility of the sidebar view’s style has been greatly improved.

Soft Deprecation

Q: Recently, I noticed that the new @ViewBuilder function is unavailable in previous versions and the deprecation message prompts me to use the new method to replace the old one. Is this a design flaw in SwiftUI’s API or did I miss something?

Swift
 @available(iOS, introduced: 13.0, deprecated: 100000.0, message: "Use `overlay(alignment:content:)` instead.")
 @inlinable public func overlay<Overlay>(_ overlay: Overlay, alignment: Alignment = .center) -> some View

Version 100000.0 of the deprecated Swift framework is a way for API authors to communicate that an API should not be used in new projects, but it is acceptable to continue using it in existing projects. These “soft deprecated” APIs are not included in code autocompletion and are usually listed separately in documentation. However, the compiler will not issue warnings for existing uses. Because these uses are not harmful, we do not want developers to deal with a lot of warnings due to using a new compiler version.

macOS API

Q: For a Mac running Monterey, what is the suggestion for achieving the following requirements in SwiftUI:

  1. Open a window
  2. Initialize data in the window
  3. Find all open windows
  4. Determine if a window is open
  5. Close a window from a view that is not in the window

A: What I want to say is that if possible, targeting macOS Ventura would be more helpful for some of these operations. Specifically, we have added new OpenWindowAction and new initialization methods on WindowGroup, which will satisfy 1 and 2 at the same time. If you can’t do this, you can use URL and handleExternalEvents to mimic some of the behavior, but it has much more limitations. There is currently no suitable API for the other points.

Chain Animation

Q: How can I achieve chain animation in SwiftUI? For instance, I want to animate one view first, and immediately start another animation when the first animation completes.

A: Unfortunately, it’s currently not possible to achieve chain animation. Based on your question, you can use animation.delay(…) to delay the second part of the animation until the first part completes. If you can provide more details about your use case, we would appreciate it.

SwiftUI currently lacks a callback mechanism after animation completion. In simple cases, you can observe the progress of the animation by creating a ViewModifier that conforms to the Animatable protocol. For more information, please refer to the tweet and code.

Too complex to type check

Q: I encountered a problem in iOS 14 SwiftUI. I was trying to conditionally display one of three objects that conform to the Shape protocol. Two of them are custom shapes (basically rounded rectangles with only two corners rounded), and one is a rectangle. The compiler throws an error stating that it took too long to check the view’s type.

A: Yes, unfortunately, large constructor expressions like this can sometimes be difficult for the Swift compiler to handle. The solution to encountering this error is to break down the expression into smaller sub-expressions, especially if these smaller sub-expressions are given explicit types.

When the structure of the view is too complex, it can be difficult to read, and there may be instances where code auto-completion cannot be used, as well as the aforementioned inability to compile (too complex to type check). A good solution is to disperse the functionality of the view into functions, smaller view structures, and view modifiers.

Switching between Text and TextField in edit mode

Q: In the editMode documentation, it is suggested that the Text view can be replaced with TextField in non-edit mode. However, the exchange between two views with identical content does not smoothly animate because the text for both is also animated. I am using an alternative method that only disables TextField, but is there a way to guide the animation to use the method in the documentation?

A: Solution: Keep the TextField, but conditionally set disabled(true) when it cannot be edited and disabled(false) when it can be edited.

Setting the correct transition style can avoid unnecessary flickering or animation.

Swift
struct ContentView: View {
    var body: some View{
        VStack {
            EditButton()
            List{
                Cell()
            }
        }
    }
}

struct Cell:View {
    @State var text = "Hello"
    @Environment(\.editMode) var editMode
    var body: some View{
        ZStack {
            if editMode?.wrappedValue == .active {
                TextField("",text: $text).transition(.identity)
            } else {
                Text(text).transition(.identity)
            }
        }
    }
}

Separating Code

Q: I’ve noticed that my view code has grown larger, not due to the actual view content, but due to the code in decorators such as sheet and toolbar. I’m currently figuring out a way to extract the toolbar content into a function annotated with @ToolbarContentBuilder. Is there a good way to extract the code in a large number of sheets and alerts?

A: You can encapsulate some of the code by creating a custom ViewModifier. Additionally, the contents of sheet and alert are both using ViewBuilders, so you can extract them into a function or computed property in a similar way to handling toolbar content.

Q&A (Compilation - Simplified Chinese)

The following questions come from discussions in the “Compilation - Simplified Chinese” channel between developers and Apple engineers (which did not appear in the English SwiftUI channel).。

Loading Core Data Images

Q: I store images using BinaryData with external storage in my CoreData. Then I use SwiftUI Image to load the images. The size of the data is quite large, and when multiple images are loaded simultaneously, it causes lagging and high memory usage. How can I improve this situation?

A: First, try to load and create images asynchronously. For example, in SwiftUI, you can use AsyncImage to load images asynchronously from a URL, or implement your own asynchronous loader according to your needs. Regarding memory usage, try to keep only the images that need to be displayed in memory, and properly handle the preloaded images. You can refer to WWDC18’s Image and Graphic Best Practices for many good suggestions on image memory optimization.

Asynchronous loading + thumbnail. For images that may cause lagging, avoid directly accessing them from the image relationship of the managed object. In the Cell view, create a request to extract data from the private context and convert it into an image. Additionally, consider creating thumbnails for the original images to further improve the display efficiency. Read Memory Optimization Journey for a SwiftUI + Core Data App to learn more about memory optimization methods.

Issue with Chinese Input in TextField

Q: May I ask if it’s a known issue that SwiftUI’s TextField will directly input the selected letters during the letter selection stage when inputting Chinese, causing input errors? Will it be fixed in 16.1 RC?

A: We were not able to reproduce the issue you mentioned on iOS 16.0.3. Can you provide relevant code snippets to help us reproduce and investigate the problem? If you have submitted this issue through Feedback Assistant, please let us know the Feedback ID.

This is a strange issue that has appeared in multiple versions. In early versions of SwiftUI, when using the system Chinese input method in iOS, it was easy to trigger this situation. But it was gradually fixed later. Recently, I also saw similar discussions in the chat room (I myself have not encountered it on iOS 16). Here is a temporary solution.

https://cdn.fatbobman.com/image-20221023171100484.png

Scrolling Speed

Q: Is there a good way to listen for velocity values when scrolling through a List or ScrollView? As of the current version of SwiftUI, you can obtain the distance of the scroll by:

  1. Customizing a struct to implement the PreferenceKey protocol, where the custom struct is responsible for collecting geometry data (view coordinate information).
  2. Calling transformAnchorPreference(key:_, value:_, transform:_) or preference(key:_,value:_) to collect coordinate information when SwiftUI updates the view.
  3. Calling onPreferenceChange(:_,perform:_) to obtain the collected coordinate information. However, this implementation does not allow for obtaining velocity.

A: May I ask what you need this velocity value for? Typically, this value is not necessary, and if you need to check for dropped frames during scrolling, you can view it in Xcode Organizer or generate a report using MetricKit. You can also use Instruments in the development environment. Therefore, I am curious as to what specific purpose you need this velocity value for. You can try recording the time changes while obtaining position changes to calculate velocity. However, if it involves user interaction, I suggest considering the sensitivity of users to velocity and the interaction effect itself, and whether there is a more convenient way to achieve this.

In SwiftUI, there is a scroll container that has existed since the first version but has not been made public - _ScrollView. This scroll container provides many API interfaces that standard ScrollView cannot provide, such as enhanced control over gestures, displacement of views within containers, and bounce control. However, this scroll has two major issues: 1. it is an unpublished prototype and may be removed from the SwiftUI framework; 2. it does not support lazy loading, even when used with Lazy views, it will load all views at once. For more information, you can check out the SolidScroll library, which has wrapped it up twice. Read How to Determine if ScrollView is Scrolling in SwiftUI to learn about several methods for determining the scrolling state.

Summary:

I overlooked the issue of not obtaining a conclusion. I hope the above summary can be helpful to you.

I'm really looking forward to hearing your thoughts! Please Leave Your Comments Below to share your views and insights.

Fatbobman(东坡肘子)

I'm passionate about life and sharing knowledge. My blog focuses on Swift, SwiftUI, Core Data, and Swift Data. Follow my social media for the latest updates.

You can support me in the following ways