BLACK FRIDAY SALE: Save 50% on all my Swift books and bundles! >>

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 save that QR code to their images.

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 {
        Button {
            // save my code
        } label: {
            Label("Save to Photos", systemImage: "square.and.arrow.down")
        }
    }

In terms of saving the image, we can use the same ImageSaver class we used back in project 13 (Instafilter), because that takes care of all the complex work for us. If you have ImageSaver.swift around from the previous project you can just drag it into your new project now, but if not here’s the code again:

import UIKit

class ImageSaver: NSObject {
    var successHandler: (() -> Void)?
    var errorHandler: ((Error) -> Void)?

    func writeToPhotoAlbum(image: UIImage) {
        UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveCompleted), nil)
    }

    @objc func saveCompleted(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
        if let error = error {
            errorHandler?(error)
        } else {
            successHandler?()
        }
    }
}

When it comes to using that we can just repeat the same code to generate our UIImage, then save that – replace the // save my code comment with this:

let image = generateQRCode(from: "\(name)\n\(emailAddress)")
let imageSaver = ImageSaver()
imageSaver.writeToPhotoAlbum(image: image)

And we’re done!

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:

Button {
    let imageSaver = ImageSaver()
    imageSaver.writeToPhotoAlbum(image: qrCode)
} label: {
    Label("Save to Photos", systemImage: "square.and.arrow.down")
}

That code will compile cleanly, but I want you to run it and see what happens.

If everything has gone to plan, Xcode should show a purple warning over your code, saying that we modified our view’s state during a view update, which causes undefined behavior. “Undefined behavior” is a fancy way of saying “this could behave in any number of weird ways, so don’t do it.”

You see, we’re telling Swift it can load our image by calling the generateQRCode() method, so when SwiftUI calls the body property it will run generateQRCode() as requested. However, while it’s running that method, we then change our new @State property, even though SwiftUI hasn’t actually finished updating the body property yet.

This is A Very Bad Idea, which is why Xcode is flagging up a large warning. Think about it: if drawing the QR code changes our @State cache property, that will cause body to loaded again, which will cause the QR code to be drawn again, which will change our cache property again, and so on – it’s really messy.

The smart thing to do here is tell our image to render directly from the cached qrImage property, then call generateQRCode() when the view appears and whenever either name or email address changes.

First, add this new method to MeView, so we can update our code from several places without having to repeat the exact string:

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

Second, revert the qrCode = UIImage(cgImage: cgimg) line in generateQRCode(), because that’s no longer needed – you can just return the UIImage directly, like before.

Third, change the QR code image to this:

Image(uiImage: qrCode)

Finally, add these new modifiers after navigationTitle():

.onAppear(perform: updateCode)
.onChange(of: name) { _ in updateCode() }
.onChange(of: emailAddress) { _ in updateCode() }

That will ensure the QR code is updated as soon as the view is shown, or whenever name or emailAddress get changed – perfect for our needs, and much safer than trying to change some state while SwiftUI is updating our view.

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 this step is done – 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.

Save 50% in my Black Friday sale.

SAVE 50% To celebrate Black Friday, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

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.7/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.