WWDC Quick Look 💓 By SwiftGGTeam
Embrace Swift type inference

Embrace Swift type inference

Watch original video

Highlight

Apple uses Fruta’s SwiftUI search list to show how Swift type inference deduce generic parameters from arrays, KeyPaths, closures, and ViewBuilders, and explains how Xcode 12 turns expression errors into diagnostics with notes and fix-its.

Core Content

SwiftUI code looks short because a lot of type information is hidden in the call site context. A list, a filter condition, a row view closure, which may involve generic parameters, KeyPath, escaping closure and@ViewBuilder. If developers wrote out all types manually, the code would be swamped with angle brackets, type conversions, and closure parameter annotations.

The smoothie app Fruta was chosen for this session. Holly wants to add a search to the list: the user enters a string, and the list only displays smoothies whose titles contain that string. She did not directly insert the filtering logic intoList, but wrote a reusableFilteredList, allowing the caller to pass in data, filtering properties, filtering functions, and row views.

The key point is the call point.FilteredList(smoothies, filterBy: \.title, isIncluded: { title in ... }) { smoothie in ... }not writtenElementFilterKeyRowContent. The compiler starts fromsmoothiesroll outElement == Smoothie, and then from\.titleroll outFilterKey == String, and finally pushed out from the trailing ViewBuilder closureRowContent == SmoothieRowView

There are also changes in Xcode 12 when type inference errors occur. Swift 5.3 extends integrated error tracking to all errors in expressions. The compiler will record problems that occur during inference, use notes to point to relevant declarations, and use fix-it to fix common errors, such as passingTextFieldChange the string value to binding.

Detailed Content

1. The call point leaves enough clues to the compiler

(02:56) Fruta’sSmoothieListSave search terms@State, and then put the originalListReplace withFilteredList. This official code shows how call sites can be kept simple.

import SwiftUI

struct SmoothieList: View {
    var smoothies: [Smoothie]

    @State var searchPhrase = ""

    var body: some View {
        FilteredList(
            smoothies,
            filterBy: \.title,
            isIncluded: { title in title.hasSubstring(searchPhrase) }
        ) { smoothie in
            SmoothieRowView(smoothie: smoothie)
        }
    }
}

extension String {
    /// Returns `true` if this string contains the provided substring,
    /// or if the substring is empty. Otherwise, returns `false`.
    ///
    /// - Parameter substring: The substring to search for within
    ///   this string.
    func hasSubstring(_ substring: String) -> Bool {
        substring.isEmpty || contains(substring)
    }
}

Key points:

  • smoothiesThe known types are[Smoothie], it givesElementThis generic parameter provides the first clue. -filterBy: \.titlebase type is not written becauseElementAlready letting the compiler know that this isSmoothie.title
  • isIncludedin closuretitleDepend onFilterKeylaunched asString, so you can callhasSubstring.
  • Trailing closure returnsSmoothieRowView, this isRowContentconcrete type. -@State var searchPhrase = ""Also relies on literal inference, which the compiler infers from the empty stringString

2. Reusable components write type relationships in declarations

03:53FilteredListThe declaration clearly states the type relationships omitted from the call point. Generic parameters represent data elements, filter fields and row view content respectively.

import SwiftUI

public struct FilteredList<Element, FilterKey, RowContent>: View
        where Element: Identifiable, RowContent: View {

    private let data: [Element]
    private let filterKey: KeyPath<Element, FilterKey>
    private let isIncluded: (FilterKey) -> Bool
    private let rowContent: (Element) -> RowContent

    public init(
        _ data: [Element],
        filterBy key: KeyPath<Element, FilterKey>,
        isIncluded: @escaping (FilterKey) -> Bool,
        @ViewBuilder rowContent: @escaping (Element) -> RowContent
    ) {
        self.data = data
        self.filterKey = key
        self.isIncluded = isIncluded
        self.rowContent = rowContent
    }

    public var body: some View {
        let filteredData = data.filter {
            isIncluded($0[keyPath: filterKey])
        }

        return List(filteredData, rowContent: rowContent)
    }
}

Key points:

  • Elementis constrained toIdentifiable, because it will ultimately be handed over to SwiftUIListexhibit. -FilterKeyfromKeyPath<Element, FilterKey>, allowing the component to filter any field. -isIncludedtake overFilterKeyand returnBool, the calling point only needs to describe “whether this field value is retained”. -rowContenttake overElementand returnRowContentwhere RowContent: ViewEnsure that the return value can become a SwiftUI view. -@ViewBuilderLet the call point use SwiftUI DSL syntax, and ViewBuilder will collect the child views in the closure into the structure required by the parent view.

3. Type inference is like solving constraints, not guessing the type

(08:43) The talk describes type inference as a puzzle. The compiler looks firstsmoothies, which is known from Quick Help[Smoothie], so allElementreplace placeholder withSmoothie. This result makes\.titlebecomeSmoothie.title, Quick Help shows it isString,thenFilterKeybecomeString. Finally, there is only one in the ViewBuilder closureSmoothieRowView,soRowContentbecomeSmoothieRowView

This sequence explains why the call site does not need to be written:

// These types are inferred from call-site context; real code does not need to write them by hand.
Element == Smoothie
FilterKey == String
RowContent == SmoothieRowView

Key points:

  • The compiler starts deducing from existing types, and new conclusions will continue to unlock subsequent clues. -ElementOnce identified, all occurrencesElementAll locations use the same concrete type. -FilterKeyThe source isKeyPathThe pointed attribute type, the closure parameter name just inherits this inferred type. -RowContentThe result type from the ViewBuilder closure.
  • If a clue fills the placeholder with a mismatched type, inference cannot be completed and there is an error in the source code.

4. Xcode 12 turns inference failures into traceable diagnostics

(11:31, [14:00](https://developer.apple.com/vi deos/play/wwdc2020/10165/?time=840), 15:46) If key path is written to the wrong attribute, the compiler mayFilterKeyPush it into placeBool. NextisIncludedCalled in a closurehasSubstringhour,BoolWithout this method, the expression fails.

Xcode 12 improvements are here. The Swift compiler logs errors during type inference, continues inference using heuristics, and finally reports the collected errors, fix-it, and notes. Two specific examples from the speech were:TextFieldRequires binding, provided by the compiler$Reference binding’s fix-it;SmoothieDoes not meetIdentifiable, note will take you back toFilteredListdeclare, seewhere Element: Identifiablethis constraint.

public struct FilteredList<Element, FilterKey, RowContent>: View
        where Element: Identifiable, RowContent: View

Key points:

  • IdentifiableThe constraints are not thereSmoothieListon the call point, but will be returned through noteFilteredListstatement.
  • The inferred concrete type recorded by the compiler will appear in the diagnostics, for exampleElementpushed intoSmoothie.
  • Issue Navigator is suitable for viewing the build failure of the entire project; Option+Shift click note to open related files side by side.
  • Quick Help can verify the inferred type of specific parameters, such as\.titlecorrespondString.
  • Use empty strings and"Berry"Two sets of search terms refresh the canvas, confirming that the filtering logic actually operates according to the inferred type.

Core Takeaways

1. Make a typed search list

What to do: Extract list searches into similarFilteredListA reusable SwiftUI component that lets callers pass in arrays, filter fields, filter conditions, and row views.

Why it’s worth doing: session showsElementFilterKeyRowContentHow to automatically infer from the call point so that the caller can stay close to SwiftUI nativeListway of writing.

How ​​to get started: Ask firstElement: Identifiable, design the field entry asKeyPath<Element, FilterKey>, the filter closure is written as(FilterKey) -> Bool, row closure plus@ViewBuilder

2. Design clearer generic boundaries for team components

What to do: Examine the internal SwiftUI component and separate the “type to be written at the call site” from the “type relationship expressed in the declaration”.

Why it’s worth doing: The reason call points in speeches are concise is becauseFilteredListThe initializer parameter fully expresses the relationship between data, KeyPath, filter closure, and row closure.

How ​​to start: Start with a recurring view component and list the three types it really needs: input data, fields used for configuration, and the final generated view; then put these types into generic parameters andwhereconstraint.

3. Incorporate compiler notes into the debugging process

What to do: Establish a set of Swift build failure handling habits: Open the Issue Navigator, look at fix-it first, and then look at the declaration file pointed to by notes.

Why it’s worth doing: Swift 5.3 and Xcode 12 will log more error context in expression type inference, with notes explaining “why this constraint is relevant to the current call site.”

How ​​to start: Set the Issue Navigator when build fails in Xcode Behaviors; when encountering a generic or SwiftUI error, use Option+Shift to open the location pointed to by note, and then return to the call point to fix it.

4. Use Preview to overwrite the real path after type inference

What to do: Supplement multiple sets of SwiftUI previews for components that rely on generics and ViewBuilder, such as empty search terms and"Berry"search term.

Why is it worth doing: Finally, the session uses preview provider to run two sets of search phrases at the same time to verifyFilteredListThe inferred type, filter function, and rowContent closure work together correctly.

How ​​to start: Prepare a set of fixed inputs for the preview provider, enumerate the key states, and go through the component’s real initializer once for each state, instead of separately bypassing the call point to test the internal logic.

  • What’s new in Swift — Learn about Swift 5.3’s same-year updates to the language, diagnostics, runtime, and development experience.
  • App essentials in SwiftUI — Put the SwiftUI view combination here into the complete App, Scene and WindowGroup structure.
  • What’s new in SwiftUI — See more SwiftUI 2020 call sites that rely on generics, closures, and ViewBuilder.
  • Introduction to SwiftUI — Return to the basics of declarative view composition and understand why SwiftUI requires so much type inference.

Comments

GitHub Issues · utterances