Search code examples
swiftuitoolbar

Convert an array of UIBarButtonItem to array of ToolbarItem


I'm new to swiftUI. We have both UIKit and swiftUI code in our project and we have some custom logic to add few UIBarButtonItem. I need to pass viewController.navigationItem.rightBarButtonItems array from UIKit code to a separate swiftUI module and then create toolBar with these rightBarButtonItems.

struct ToolBarItems: ToolbarContent {

    let barButtons: [UIBarButtonItem]
    init(barButtons: [UIBarButtonItem]) {
        self.barButtons = barButtons
    }
    
    var body: some ToolbarContent {
        ToolbarItem(placement: .principal) {
            Text("principal Title")
        }
    }
    
    @ToolbarContentBuilder
    var toolbarButtons: some ToolbarContent {
        ForEach(barButtons) { item in
            ToolbarItem(placement: .navigationBarTrailing) {
                Button(item.title!) {
                    
                }
            }
        }
    }
}

This gives errors "No exact matches in reference to static method 'buildExpression'". Not sure how to do this conversion. Please advise.


Solution

  • The error is because ForEach is not ToolbarContent. You can create a ToolbarItemGroup instead, which takes a @ViewBuilder, and you can put the ForEach in the @ViewBuilder:

    struct UIKitBarButtons: ToolbarContent {
        ...
    
        var body: some ToolbarContent {
            ToolbarItemGroup(placement: .topBarTrailing) {
                ForEach(barButtons) { item in
                    Button(item.title!) { ... }
                }
            }
        }
    }
    

    Also, you would need extension UIBarButtonItem: Identifiable {} for the ForEach to work.

    However, item.title is main actor-isolated, but ToolbarContent.body is not. While you can isolate ToolbarContent.body to @MainActor, you would end up implementing a non-isolated protocol requirement with an actor-isolated property, which is potentially not safe.

    I would create my own struct to represent a "navigation bar item". For example, if you are only interested in the title, image, and action of the UIBarButtonItem, I would create a struct like this:

    struct NavBarButton: Identifiable {
        let id: ObjectIdentifier
        let title: LocalizedStringKey?
        let image: UIImage?
        let action: () -> Void
        
        @MainActor
        init(_ barButtonItem: UIBarButtonItem) {
            id = ObjectIdentifier(barButtonItem)
            title = barButtonItem.title.map(LocalizedStringKey.init(_:))
            image = barButtonItem.image
            self.action = {
                guard let selector = barButtonItem.action else {
                    return
                }
                UIApplication.shared.sendAction(selector, to: barButtonItem.target, from: nil, for: nil)
            }
        }
    }
    

    You can use this in a custom ToolbarContent like this:

    struct UIKitBarButtons: ToolbarContent {
        let barButtons: [NavBarButton]
    
        var body: some ToolbarContent {
            ToolbarItemGroup(placement: .topBarTrailing) {
                ForEach(barButtons) { item in
                    Button {
                        item.action()
                    } label: {
                        if let title = item.title {
                            Text(title)
                        }
                        if let image = item.image {
                            Image(uiImage: image)
                        }
                    }
                }
            }
        }
    }
    

    In the body of a View, you can then do:

    .toolbar {
        UIKitBarButtons(barButtons: uiBarButtonItems.map(NavBarButton.init))
    }
    

    NavBarButton.init is main actor-isolated, but so is View.body, so this is okay.