SwiftUI Diagnostics Plugin: Coming Soon

One thing that has always bothered me about SwiftUI is the fact that a lot of code that compiles successfully doesn't work properly during execution. There are many silent mistakes that beginners can make and no clue how to correct them.

Because of this I have been developing a plugin to detect a series of common errors during compilation. In the list below you can check all the diagnostics that I have working at the moment. These are mistakes that I myself have made or observed in my work as an educator over the years.

If you are interested in this, I would be very happy if you liked this post. My plan is to release this plugin mid to late February and until then I would love suggestions for new diagnostics to include in this initial version.

Diagnostics

Missing Modifier Dot


var body: some View {
    Rectangle()
        padding() // ❌  Missing 'padding' leading dot
}

Non-Grouped Views


var body: some View { // ⚠️ Use a container view to group 'Image' and 'Text'
    Image(systemName: "globe")
    Text("Hello, world!")
}

NavigationStack { // ⚠️ Use a container view to group 'Image' and 'Text'
    Image(systemName: "globe")
    Text("Hello, world!")
}


Stack Child Count

Includes VStack, HStack, ZStack, NavigationStack


VStack { // ⚠️ 'VStack' has only one child; consider using 'Color' on its own
    Color.red
}

HStack { // ⚠️ 'HStack' has no children; consider removing it
    
}

Control Label

Includes Button, NavigationLink, Link and Menu


NavigationLink {
    
} label: {
    Button("Button") { } // ⚠️ 'Button' should not be placed inside 'NavigationLink' label
}

Sheet Dismiss


struct A: View {

    @State private var isShowingSheet = true

    var body: some View {
        Button("B") {
            isShowingSheet = true
        }
        .sheet(isPresented: $isShowingSheet) {
            B(isPresented: $isShowingSheet)
        }
    }
    
}

struct B: View {

    @Binding var isPresented: Bool

    var body: some View {
        Button("Dismiss") {
            isPresented = false // ⚠️ Dismiss 'SheetContent' using environment 'DismissAction' instead
        }
    }
    
}

Observable Object Initialization


@ObservedObject var model = Model() // ⚠️ ObservedObject should not be used to create the initial instance of an observable object; use 'StateObject' instead

Missing Environment Object


struct A: View {

    @StateObject private var model = Model()

    var body: some View {
        B()
    }
    
}

struct B: View {

    @EnvironmentObject private var model: Model

    var body: some View {
        Text(verbatim: "\(model)") // ⚠️ Insert object of type 'Model' in environment with 'environmentObject' up in the hierarchy
    }
    
}

Image

Invalid System Symbol

Image(systemName: "xyz") // ⚠️ There's no system symbol named 'xyz'

Missing Resizable


Image("...")
    .frame(width: 100, height: 100) // ⚠️ Missing 'resizable' modifier
    
Image("...")
    .scaledToFit() // ⚠️ Missing 'resizable' modifier
    
Image("...")
    .scaledToFill() // ⚠️ Missing 'resizable' modifier

State

State Access Control

struct ContentView: View {
    
    @State var username = "" // ⚠️ Variable 'username' should be declared as private to prevent unintentional memberwise initialization
    
    var body: some View {
        TextField("Username", text: $username)
    }
    
}

State Mutation

struct ContentView: View {
    
    @State private var author = "Hamlet" // ⚠️ Variable 'author' was never mutated or used to create a binding; consider changing to 'let' constant
    
    var body: some View {
        Text(author)
    }
    
}

State Class Value

class Model { }

struct ContentView: View {
    
    @State private var model = Model() // ⚠️ Mark 'Model' type with '@Observable' or use 'StateObject' instead
    
    var body: some View {
        Color.red
    }
    
}

Navigation

Misplaced Navigation Modifiers

struct ContentView: View {

    var body: some View {
        NavigationStack {
            Text("ContentView")
        }
        .navigationTitle("Title") // ⚠️ Misplaced 'navigationTitle' modifier; apply it to NavigationStack content instead
    }
    
}

Missing NavigationStack


struct A: View {

    var body: some View {
        NavigationLink("B", destination: B()) // ⚠️ 'NavigationLink' only works within a 'NavigationStack' hierarchy
    }
    
}

struct B: View {

    var body: some View {
        NavigationLink("C", destination: C()) // ⚠️ 'NavigationLink' only works within a 'NavigationStack' hierarchy
    }
    
}

Nested NavigationStack

struct A: View {

    var body: some View {
        NavigationStack {
            NavigationLink("B", destination: B()) // ⚠️ 'B' should not contain a NavigationStack
        }
    }
    
}

struct B: View {

    var body: some View {
        NavigationStack {
            NavigationLink("C", destination: C())
        }
    }
    
}

Navigation Loop

struct A: View {

    var body: some View {
        NavigationStack {
            NavigationLink("B", destination: B())
        }
    }
    
}

struct B: View {
    
    var body: some View {
        NavigationLink("C", destination: C())
    }
    
}

struct C: View {
        
    var body: some View {
        VStack {
            NavigationLink("A", destination: A()) // ⚠️  To go back more than one level in the navigation stack, use NavigationStack 'init(path:root:)' to store the navigation state as a 'NavigationPath', pass it down the hierarchy and call 'removeLast(_:)'
            NavigationLink("B", destination: B()) // ⚠️ To navigate back to 'B' use environment 'DismissAction' instead
        }
    }
    
}

List

Misplaced List Modifiers

List) {
                
}
.listRowBackground(Color.red) // ⚠️ Misplaced 'listRowBackground' modifier; apply it to List rows instead

Selection Type Mismatch


struct ContentView: View {

    @State private var selection: String? = ""

    private let values = [1, 2, 3, 4, 5]

    private let models: [Model] = [Model(), Model(), Model(), Model(), Model()]

    var body: some View {
        List(selection: $selection) {
        
            Text("0")
                .tag(0) // ⚠️ tag value '0' type 'Int' doesn't match 'selection' type 'String'
        
            ForEach(1..<5) { // ⚠️ 'ForEach' data element 'Int' doesn't match 'selection' type 'String'
                Text("\($0)")
            }
            
            ForEach([1, 2, 3, 4, 5], id: \.self) { // ⚠️ 'ForEach' data element 'Int' doesn't match 'selection' type 'String'
                Text("\($0)")
            }
            
            ForEach(values, id: \.self) { // ⚠️ 'ForEach' data element 'Int' doesn't match 'selection' type 'String'
                Text("\($0)")
            }
            
            ForEach(models) { // ⚠️ ForEach' data element 'Model' id type 'UUID' doesn't match 'selection' type 'String'
                Text("\($0.id)")
            }
            
            ForEach(models, id: \.name) { // ⚠️ ForEach' data element 'Model' member 'name' type 'String' doesn't match 'selection' type 'Int'
                Text("\($0.name)")
            }
            
        }
    }

}


Picker

Unsupported Multiple Selections


struct ContentView: View {
        
    @State private var selection: Set<Int> = []
    
    var body: some View {
        Picker("Picker", selection: $selection) { // ⚠️ 'Picker' doesn't support multiple selections
            ...
        }
    }
    
}


Selection Type Mismatch


struct ContentView: View {
        
    @State private var selection: Int?
    
    private let values = [1, 2, 3, 4, 5]
    
    var body: some View {
        Picker("Picker", selection: $selection) {

            Text("None") // ⚠️ Apply 'tag' modifier with 'Int?' value to match 'selection' type
            
            Text("0")
                .tag("0") // ⚠️ tag value 'a' type 'String' doesn't match 'selection' type 'Int?'

            ForEach(1..<5) {
                Text("Row \($0)") // ⚠️ Apply 'tag' modifier with explicit Optional<Int> value to match 'selection' type 'Int?'
            }
            
            ForEach(["a", "b", "c"], id: \.self) { // ⚠️ 'ForEach' data element 'String' doesn't match 'selection' type 'Int?'
                Text("Value \($0)")
            }
                
        }
    }
    
}
20 Likes

Looks awesome! I can definitely see this being very useful for people learning SwiftUI, many of those misconceptions would be great to correct early.

2 Likes

This looks fantastic!

Could you please reply on this thread whenever there's developments? I subscribed for updates :)

2 Likes

Thanks for the interest! In the last weeks I ended up derailing my focus to implementing trailing comma :sweat_smile: but I will soon find the time to wrap up a initial release for this plugin.

2 Likes

Just a heads up that I'm back working on this and I hope to release it soon :crossed_fingers:t2:

8 Likes