3. August 2019

Tutorial: Apps mit UIContextMenuInteraction um Kontextmenüs erweitern

In iOS 13 gibt es ein neues Standard-UI für Kontextmenüs, welches das alte Peek and Pop-Feature ersetzt. Diese werden auf 3D-Touch-fähigen Geräten durch einen kräftigen Force-Touch ausgelöst, auf allen anderen Geräten durch einen etwas länger gehaltenen Long Press:

Beispiel-App Kontextmenü

Wird eine App mit Catalyst für macOS kompiliert, wird das Kontextmenü entsprechend ähnlich zu einer AppKit-Anwendung dargestellt:

Kontextmenü unter macOS

Das Tutorial zeigt, wie ein UI mit einer UIContextMenuInteraction um ein solches Kontextmenü erweitert wird:

  1. Verwende für das Tutorial die Xcode 11 Beta-Version (dieses Tutorial wurde zuletzt getestet mit Beta 7).

  2. Lade das Start-Beispielprojekt von dem Countries-Projekt.

    Dieses Projekt basiert auf dem UICollectionView-Tutorial und enthält ein UI, welches im Folgenden um Kontextmenüs erweitert wird.

    Mache Dich mit dem Beispielprojekt vertraut: GalleryCollectionViewController enthält ein Collection-View-Controller den die App initial anzeigt, CountryViewController ist die Folge-Sicht wenn eine Fotokachel getappt wird.

  3. Erstelle für die Implementierung eine neue Swift-Datei CountryControllers+ContextMenu.swift.

  4. Implementiere hier eine Funktion createContextMenu, die eine UIContextMenuConfiguration erstellt. Erzeuge ein UIMenu mit einer UIAction, die die Sehenswürdigkeit zu dem Land (repräsentiert durch ein Country) in der Apple Maps-App öffnet (dazu enthält das Projekt bereits eine Extension Country+Maps.swift, die das Öffnen in Maps erledigt):

    private func createContextMenu(country: Country) -> UIContextMenuConfiguration {
    
        let actionProvider: UIContextMenuActionProvider = { _ in
    
            let openInMaps = UIAction(title: "Auf Karte zeigen", image: UIImage(systemName: "mappin.and.ellipse")) { _ in
                country.openInMaps()
            }
    
            return UIMenu(title: country.landmark.name, children: [openInMaps])
        }
    
        return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: actionProvider)
    }

    Tipp: Die Symbole stammen aus der neuen SF Symbols Schrift, die mit der SF Symbols-App nachgeschlagen werden können:

    Sf Symbols

  5. Dies sollte zu einem Compilerfehler 'UIContextMenuConfiguration' is only available in iOS 13.0 or newer führen. Du kannst dieses Problem beheben, indem Du die Funktion nur für iOS 13 deklarierst:

    @available(iOS 13.0, *)
    private func createContextMenu(country: Country) -> UIContextMenuConfiguration {
        // ...
    }

    oder das Mindest-Deployment-Target der App auf iOS 13 setzt:

    Deployment Target auf iOS 13 konfigurieren
  6. Erstelle eine Extension für den CountryViewController, die diesen konform zu dem Protokoll UIContextMenuInteractionDelegate macht und implementiere die Methode contextMenuInteraction(_:, configurationForMenuAtLocation:). Stelle zudem eine Methode enableContextMenu bereit, die dem imageView des Controllers eine UIContextMenuInteraction setzt:

    @available(iOS 13.0, *)
    extension CountryViewController: UIContextMenuInteractionDelegate {
    
        func enableContextMenu() {
            imageView.isUserInteractionEnabled = true
            imageView.addInteraction(UIContextMenuInteraction(delegate: self))
        }
    
        func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
            guard let country = country else { return nil }
            return createContextMenu(country: country)
        }
    
    }
  7. So ist die Logik zur Bereitstellung des Kontext-Menüs komplett separat abgelegt, lediglich der Aufruf von enableContextMenu muss in der CountryViewController-Klasse selbst erfolgen:

    class CountryViewController: UIViewController {
    
        // ...
    
        override func viewDidLoad() {
            super.viewDidLoad()
            if #available(iOS 13.0, *) {
        self.enableContextMenu()
    }
        }
    
        // ...
    
    }
  8. Erstelle eine weitere Extension, um die Klasse GalleryCollectionViewController um die collectionView(_:, contextMenuConfigurationForItemAt:)-Methode zu erweitern (UITableViewController und UICollectionViewController werden über diese Delegate-Methode um ein Kontextmenü erweitert):

    @available(iOS 13.0, *)
    extension GalleryCollectionViewController {
    
        override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
            let country = self.countries[indexPath.row]
            return createContextMenu(country: country)
        }
    
    }
  9. Starte die App und teste, dass das Kontextmenü in beiden View-Controllern angezeigt wird (Long Press auf eines der Fotos):

    Beispiel-App Kontextmenü

  10. Sofern Du schon macOS Catalina verwendest: Konfiguriere die App im Target kompatibel zu Mac:

    Target Device Mac

    Wähle als Schema My Mac aus und starte die App. Hier wird das Kontextmenü als natives macOS-Kontextmenü angezeigt:

    Kontextmenü unter macOS

  11. Aufgabe: Ergänze das Kontext-Menü um eine weitere UIAction, um das Foto zu teilen. Das Share-Sheet dazu kann über die UIActivityViewController-Klasse angezeigt werden:

    let shareSheet = UIActivityViewController(activityItems: [country.image], applicationActivities: nil)
    self?.present(shareSheet, animated: true, completion: nil)