Home > Software engineering >  SwiftUI Button compiler bug or mine?
SwiftUI Button compiler bug or mine?

Time:11-27

I have the following code:

import SwiftUI

enum OptionButtonRole {
    case normal
    case destructive
    case cancel
}

struct OptionButton {
    var title: String
    var role: OptionButtonRole
    let id = UUID()
    var action: () -> Void
}

struct OptionSheet: ViewModifier {
    @State var isPresented = true
    var title: String
    var buttons: [OptionButton]
    
    init(title: String, buttons: OptionButton...) {
        self.title = title
        self.buttons = buttons
    }
    
    func body(content: Content) -> some View {
        content
            .confirmationDialog(title, 
                                isPresented: $isPresented, 
                                titleVisibility: .visible) {
                ForEach(buttons, id: \.title) { button in
                    let role: ButtonRole? = button.role == .normal ? nil : button.role == .destructive ? .destructive : .cancel
                    Button(button.title, role: role, action: button.action)
                }
            }
    }
}

It builds and my app shows the option sheet with the specified buttons.

However, if I use an alternative Button.init, i.e. if I replace the body with the following code:

func body(content: Content) -> some View {
    content
        .confirmationDialog(title, 
                            isPresented: $isPresented, 
                            titleVisibility: .visible) {
            ForEach(buttons, id: \.title) { button in
                let role: ButtonRole? = button.role == .normal ? nil : button.role == .destructive ? .destructive : .cancel
                Button(role: role, action: button.action) {
                    Text(button.title)
                }
            }
        }
}

Then, Xcode hangs on build with the following activity:

enter image description here

Is there an error in my code or is this a compiler bug (Xcode Version 14.1 (14B47b))?

CodePudding user response:

While your code is technically correct, the ability of view logic to evaluate variable values can get quite compiler-intensive, especially when you have multiple chained ternaries and logic inside a ForEach (which seems to make a bigger difference than one would probably think).

I'd be tempted to move the conditional logic outside the loop altogether, so that you're calling a method rather than needing to evaluate and store a local variable. You could make this a private func in your view, or as an extension to your custom enum. For example:

extension OptionButtonRole {
  var buttonRole: ButtonRole? {
    switch self {
      case .destructive: return .destructive
      case .cancel: return .cancel
      default: return nil
    }
  }
}

// in your view

ForEach(buttons, id: \.title) { button in
  Button(role: button.role.buttonRole, action: button.action) {
    Text(button.title)
  }
}
  • Related