Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save analogpotato/c47794e9a6a7f12a51b71bf3bee6b3cf to your computer and use it in GitHub Desktop.
Save analogpotato/c47794e9a6a7f12a51b71bf3bee6b3cf to your computer and use it in GitHub Desktop.
Using UIViewRepresentable to declare a UIKit Menu from a button
//In your view, declare a UIViewRepresentable as a UIButton type as seen below.
//This is important as you cannot do it as a UIBarButtonItem (at least as I've found in Xcode 12 beta 2).
struct MenuButtonView: UIViewRepresentable {
typealias UIViewType = UIButton
let saveAction = UIAction(title: "") { action in }
let saveMenu = UIMenu(title: "", children: [
UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
//code action for menu item
},
UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
//code action for menu item
},
UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
//code action for menu item
},
])
func makeUIView(context: Context) -> UIButton {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
button.showsMenuAsPrimaryAction = true
button.menu = saveMenu
return button
}
func updateUIView(_ uiView: UIButton, context: Context) {
uiView.setImage(UIImage(systemName: "plus"), for: .normal)
}
}
// Once you have declared a UIViewRepresentable, it works just like a SwiftUI view and thus can be declared anywhere you would need that view.
//Declare it as MenuButtonView()
//In a toolbar you can declare it as follows:
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
SaveButtonView()
}
}
}
//The only issue I have found is in placing any code in the actions of the button, I can't seem to figure out how to get it to push to another view if it's in Swift UI.
//If you find out how to push to another SwiftUI view, let me know! It's been stumping me for awhile!
@nickkohrn
Copy link

Below are two types of navigation, which are activated by the menu. ContentView_Sheet presents the new view as a sheet, and ContentView_NavigationLink pushes the new view onto the navigation stack. ContentView_NavigationLink feels a little hacky because a hidden NavigationLink is used to get around a crash that occurs when a NavigationLink is embedded in a NavigationView's navigationBarItems.

import SwiftUI

enum DetailView: Int {
    
    case one = 1,
         two,
         three
    
}

struct MenuButtonView: UIViewRepresentable {
    
    // MARK: Type Aliases
    
    // Use the type alias to make delcarations cleaner since all actions should accept a closure that doesn't return anything.
    typealias Action = () -> Void
    
    // MARK: Properties
    
    // Call sites must provide an action for the first menu item.
    let firstAction: Action
    
    // Call sites must provide an action for the second menu item.
    let secondAction: Action
    
    // Call sites must provide an action for the third menu item.
    let thirdAction: Action
    
    // Call sites must provide an action for the save menu item.
    let saveAction: Action
    
    private var firstMenuAction: UIAction {
        UIAction(title: "First Menu Item", image: UIImage(systemName: "1.circle.fill")) { action in
            // Call the action that corresponds to this menu action.
            firstAction()
        }
    }
    
    private var secondMenuAction: UIAction {
        UIAction(title: "Second Menu Item", image: UIImage(systemName: "2.circle.fill")) { action in
            // Call the action that corresponds to this menu action.
            secondAction()
        }
    }
    
    private var thirdMenuAction: UIAction {
        UIAction(title: "Third Menu Item", image: UIImage(systemName: "3.circle.fill")) { action in
            // Call the action that corresponds to this menu action.
            thirdAction()
        }
    }
    
    private var saveMenuAction: UIAction {
        UIAction(title: "Save") { action in
            // Call the action that corresponds to this menu action.
            saveAction()
        }
    }
    
    private var saveMenu: UIMenu {
        UIMenu(title: "", children: [firstMenuAction, secondMenuAction, thirdMenuAction, saveMenuAction])
    }
    
    // MARK: UIViewRepresentable
    
    func makeUIView(context: Context) -> UIButton {
        let button = UIButton()
        button.showsMenuAsPrimaryAction = true
        button.menu = saveMenu
        return button
    }
    
    func updateUIView(_ uiView: UIButton, context: Context) {
        uiView.setImage(UIImage(systemName: "plus"), for: .normal)
    }
    
}

struct ContentView_Sheet: View {
    
    // MARK: Properties
    
    // This corresponds to which detail view to show, which can be anything. For this example, I use an enumeration, which contains identifiers of possible detail views that can be shown from the menu.
    @State private var detailViewToShow: DetailView?
    
    // This corresponds to whether a detail view should be shown.
    @State private var showDetail = false
    
    var body: some View {
        NavigationView(content: {
            Text("Hello, world!").padding()
                .onChange(of: detailViewToShow) { value in
                    // Observe when `detailViewToShow` is updated to a non-nil value. A non-nil value corresponds to which detail view to show.
                    if let _ = detailViewToShow {
                        // Set `showDetail` to `true` only if the value is non-nil. Otherwise, an empty detail view will be shown when this view appears.
                        showDetail = true
                    }
                }
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        HStack {
                            MenuButtonView(firstAction: {
                                // Pass the closure that you want executed when the first menu action is activated.
                                // In this implementation, it simply sets the first detail view to be shown.
                                detailViewToShow = .one
                            }, secondAction: {
                                // Pass the closure that you want executed when the first second action is activated.
                                // In this implementation, it simply sets the second detail view to be shown.
                                detailViewToShow = .two
                            }, thirdAction: {
                                // Pass the closure that you want executed when the third menu action is activated.
                                // In this implementation, it simply sets the third detail view to be shown.
                                detailViewToShow = .three
                            }, saveAction: {
                                // Pass the closure that you want executed when the save menu action is activated.
                                print("Saving...")
                                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                                    print("Saved!")
                                }
                            })
                        }
                    }
                }
                .sheet(isPresented: $showDetail) {
                    if let detail = detailViewToShow {
                        // The detail view should be be shown.
                        // In this implementation, the raw value is displayed on the detail view.
                        Text("View \(detail.rawValue)")
                    }
                }
        })
    }
    
}

struct ContentView_NavigationLink: View {
    
    // MARK: Properties
    
    @State private var detailViewToShow: DetailView?
    
    @State private var showDetail = false
    
    var body: some View {
        NavigationView(content: {
            HStack {
                Text("Hello, world!").padding()
                // At this time, using a `NavigationLink` inside of a navigation bar causes a crash when popping the pushed view.
                // Use a hidden `NavigationLink` somewhere in the view, but outisde of the navigation bar. You can use an `EmptyView` so that nothing
                // appears, effectively hiding it. Then, you can pass a dynamic value for the `destination` parameter by using a function to return
                // whatever type of view you want pushed, depending on what menu item is selected.
                NavigationLink(destination: detailView(for: detailViewToShow), isActive: $showDetail) {
                    EmptyView()
                }.isDetailLink(false)
            }
            .onChange(of: detailViewToShow) { value in
                if let _ = detailViewToShow {
                    showDetail = true
                }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    HStack {
                        MenuButtonView(firstAction: {
                            detailViewToShow = .one
                        }, secondAction: {
                            detailViewToShow = .two
                        }, thirdAction: {
                            detailViewToShow = .three
                        }, saveAction: {
                            print("Saving...")
                            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                                print("Saved!")
                            }
                        })
                    }
                }
            }
        })
    }
    
    // MARK: User Interface Helpers
    
    @ViewBuilder private func detailView(for detailViewType: DetailView?) -> some View {
        switch detailViewType {
        case .one:
            Text("View 1")
        case .two:
            Button {
                print("You are looking at View 2")
            } label: {
                Text("View 2")
            }
        case .three:
            Color.red
        default:
            Text("Default View")
        }
    }
    
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment