UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Adding a context menu to an image

Paul Hudson    @twostraws   

We’ve already written code that dynamically generates a QR code based on the user’s name and email address, but with a little extra code we can also let the user share that QR code outside the app. This is another example of where ShareLink comes in handy, although this time we'll place it inside a context menu.

Start by opening MeView.swift, and adding the contextMenu() modifier to the QR code image, like this:

Image(uiImage: generateQRCode(from: "\(name)\n\(emailAddress)"))
    .interpolation(.none)
    .resizable()
    .scaledToFit()
    .frame(width: 200, height: 200)
    .contextMenu {
        let image = generateQRCode(from: "\(name)\n\(emailAddress)")

        ShareLink(item: Image(uiImage: image), preview: SharePreview("My QR Code", image: Image(uiImage: image)))
    }

As you can see, we need to convert the UIImage of our QR code to a SwiftUI Image view, which can then be handed to the system's share sheet.

We could save a little work by caching the generated QR code, however a more important side effect of that is that we wouldn’t have to pass in the name and email address each time – duplicating that data means if we change one copy in the future we need to change the other too.

To add this change, first add a new @State property that will store the code we generate:

@State private var qrCode = UIImage()

Now modify generateQRCode() so that it quietly stores the new code in our cache before sending it back:

if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
    qrCode = UIImage(cgImage: cgimg)
    return qrCode
}

And now our context menu button can use the cached code:

.contextMenu {
    ShareLink(item: Image(uiImage: qrCode), preview: SharePreview("My QR Code", image: Image(uiImage: qrCode)))
}

Before you try the context menu yourself, make sure you add the same project option we had for the Instafilter project – you need to add a permission request string to your project’s configuration options.

In case you’ve forgotten how to do that, here are the steps you need:

  • Open your target settings
  • Select the Info tab
  • Right-click on an existing option
  • Choose Add Row
  • Select “Privacy - Photo Library Additions Usage Description” for the key name.
  • Enter “We want to save your QR code.” as the value.

And now go ahead and run the app – you're likely to find things don't work quite as planned. In fact, back in Xcode you might see a purple warning line in the generateQRCode() method: "Modifying state during view update, this will cause undefined behavior."

What this means is that our current view body calls generateQRCode() to create the shareable image that we're attaching our context menu to, but calling that method now saves a value in the qrCode property we marked with @State, which in turn causes the view body to be reinvoked – it creates a loop, so SwiftUI bails out and flags a big warning for us.

To fix this we need to make the Image view use our cached QR code, like this:

Image(uiImage: qrCode)

And then use a combination of onAppear() and onChange() to make sure the code is updated when the view is first shown, and also when either the name or email address changes.

This means creating a new method that updates our code in one place:

func updateCode() {
    qrCode = generateQRCode(from: "\(name)\n\(emailAddress)")
}

Then attaching some extra modifiers below navigationTitle():

.onAppear(perform: updateCode)
.onChange(of: name, updateCode)
.onChange(of: emailAddress, updateCode)

Tip: Now that updateCode() updates the value of qrCode directly, we can go back to the earlier version of generateQRCode(), which simply returns the new value:

if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
    return UIImage(cgImage: cgimg)
}

Now this step is done, and done properly – you should be able to run the app, switch to the Me tab, then long press the QR code to bring up your new context menu.

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.5/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.