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

Solution for Project 13 of 100 Days of SwiftUI using new PhotosPicker view available with IOS 16

Forums > 100 Days of SwiftUI

I see that IOS 16 has introduced the PhotosPicker view as an alternative to wrapping UIKit PHPickerViewController using UIViewControllerRepresentable. So I have modifed the Instafilter project to use PhotosPicker. A full solution would be to check the version of IOS and use the UIKit solution for IOS versions less than 16. But I was just curious if I could get PhotosPicker to function properly. So here is my ContentView.swift file:

//
//  ContentView.swift
//  Instafilter
//
//  Created by Michael Knych on 11/2/22.
//

import CoreImage
import CoreImage.CIFilterBuiltins
import PhotosUI
import SwiftUI

//--------------------------------------------------------------
// Instafilter:  Presents a button to select an image from the
// user's photo library and then display the selected image in the
// display area of the screen with a defualt filter of sepia
// and a defualt filter intensity of 0.7.
// The user can then select an alternative image filter from a presented
// list which will also show sliders that represent the input
// keys assocaited with the image filter when it's selected.
// There is also a save button that will save the ddisplayed filtered
// image into the user's photo library.
//--------------------------------------------------------------

struct ContentView: View {

  @State var selectedItem: PhotosPickerItem?
  @State var selectedPhotoData: Data?

  @State private var image: Image?
  @State private var filterIntensity = 0.7
  @State private var filterRadius = 4.0
  @State private var filterScale = 6.0

  @State private var isEditing = false

  // @State private var showingImagePicker = false
  @State private var inputImage: UIImage?
  @State private var processedImage: UIImage?

  // This is a Core Image filter and a context
  @State private var currentFilter: CIFilter = CIFilter.sepiaTone()
  let context = CIContext()

  @State private var showingFilterSheet = false
  @State private var showingSaveError = false

  var body: some View {
    NavigationView {
      VStack {

        //------------------------------------------------------
        // Create the image display area as a ZStack with an
        // initial background set for the image display area
        //------------------------------------------------------
        ZStack {
          Rectangle()
            .fill(.secondary)

          Text("Tap the Button below to select an image")
            .foregroundColor(.white)
            .font(.headline)

          image?
            .resizable()
            .scaledToFit()
        }
        //        .onTapGesture {
        //          showingImagePicker = true
        //        }

        //---------------------------------------------------------
        // Use the new PhotosPicker in ios 16 instead of the
        // custom ImagePicker view
        //----------------------------------------------------------
        PhotosPicker(
          selection: $selectedItem,
          matching: .images
        )
        {
          Label("Pick an image",systemImage: "photo")
            .font(.headline)
        }
        .tint(.green)
        .controlSize(.small)
        .buttonStyle(.borderedProminent)
        .onChange(of: selectedItem) { newValue in
          Task {
            if let data = try? await newValue?.loadTransferable(type: Data.self) {
              selectedPhotoData = data
              if let selectedPhotoData = selectedPhotoData {
                inputImage = UIImage(data: selectedPhotoData)
              }
            }
          }
        }

        //-------------------------------------------------------
        // Show sliders for the input keys asscciated with the
        // current filter selected
        //-------------------------------------------------------
        if currentFilter.inputKeys.contains(kCIInputIntensityKey) {
          // Intesity slider HStack
          HStack {
            Text("Intensity")
            VStack {
              Slider(value: $filterIntensity,
                     onEditingChanged: { editing in
                isEditing = editing
              }
              )
              .onChange(of: filterIntensity) { _ in
                applyProcessing()
              }
              Text("\(filterIntensity)")
                .foregroundColor(isEditing ? .red : .blue)
            }
          }  // end of Intensity slider HStack
          .padding(.vertical)
        }
        if currentFilter.inputKeys.contains(kCIInputRadiusKey) {
          // Radius slider HStack
          HStack {
            Text("Radius")
            VStack {
              Slider(value: $filterRadius, in: 0...200,
                     onEditingChanged: { editing in
                isEditing = editing
              }
              )
              .onChange(of: filterRadius) { _ in
                applyProcessing()
              }
              Text("\(filterRadius)")
                .foregroundColor(isEditing ? .red : .blue)
            }
          }  // end of Radius slider HStack
          .padding(.vertical)
        }
        if currentFilter.inputKeys.contains(kCIInputScaleKey) {
          // Scale slider HStack
          HStack {
            Text("Scale")
            VStack {
              Slider(value: $filterScale, in: 0...10,
                     onEditingChanged: { editing in
                isEditing = editing
              }
              )
              .onChange(of: filterScale) { _ in
                applyProcessing()
              }
              Text("\(filterScale)")
                .foregroundColor(isEditing ? .red : .blue)
            }
          }  // end of Scale slider HStack
          .padding(.vertical)
        }

        HStack {
          Button("Change filter") {
            showingFilterSheet = true
          }

          Spacer()

          Button("save", action: save)
            .disabled(inputImage == nil)

        }

      }  // end of main layout VStack
      .padding([.horizontal, .bottom])
      .navigationTitle("Instafilter")
      //---------------------------------------------------------------------
      // Run loadImage whenever the state of inputImage changes
      //---------------------------------------------------------------------
      .onChange(of: inputImage) { _ in loadImage() }
      // present a sheet whenver showingImagePicker is true and run
      // our custom ImagerPicker View
      // .sheet(isPresented: $showingImagePicker) {
      //  ImagePicker(image: $inputImage)
      // }
      //---------------------------------------------------------------------
      // Confirmation dialog to allow the user to select a filter to apply
      // to the image
      //---------------------------------------------------------------------
      .confirmationDialog("Select a filter", isPresented: $showingFilterSheet) {
        Group {
          Button("Crystallize") { setFilter(CIFilter.crystallize()) }
          Button("Edges") { setFilter(CIFilter.edges()) }
          Button("Gaussian Blur") { setFilter(CIFilter.gaussianBlur()) }
          Button("Pixellate") { setFilter(CIFilter.pixellate()) }
          Button("Sepia Tone") { setFilter(CIFilter.sepiaTone()) }
          Button("Unsharp Mask") { setFilter(CIFilter.unsharpMask()) }
          Button("Vignette") { setFilter(CIFilter.vignette()) }
          Button("Pointillize") { setFilter(CIFilter.pointillize()) }
          Button("Bloom") { setFilter(CIFilter.bloom()) }
          Button("Noir") { setFilter(CIFilter.photoEffectNoir()) }

        }

        Group {
          Button("Cancel", role: .cancel) { }
        }
      }
      .alert("Error saving image", isPresented: $showingSaveError) {
        Button("OK") { }
      } message: {
        Text("Sorry there was an error saving your image.  Please check that you have allowed permissions for the app to save images to your photo library")
      }

    } // end of Navigation View
  }

  //--------------------------------------------------------------
  // Set the inputImage of UIIKMage type into the image for the
  // CI IMage filter (currentFilter) and then call the applyProccesing()
  // method that will apply filtering to the image that has been
  // set in currentFilter
  //---------------------------------------------------------------
  func loadImage() {
    // Check to see if we actually have an image in inputImage
    guard let inputImage = inputImage else { return }

    // If so, convert the UIImage into a CIImage
    // set the CIImage in the context
    // run the applyProcessing to apply the filter(s)
    let beginImage = CIImage(image: inputImage)
    currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
    applyProcessing()

  }

  //--------------------------------------------------------------
  // Will process filters to whatever image is imported by
  // setting it in curretFlter.setValue with the
  // forkey: kCIInputImageKey
  // (see the code that calls this method)
  //--------------------------------------------------------------
  func applyProcessing() {

    // Check for various filter keys and set the appropriate key value for
    // the filter set in the currentFilter
    let inputKeys = currentFilter.inputKeys

    print("\(inputKeys)")

    if inputKeys.contains(kCIInputIntensityKey) {
      currentFilter.setValue(filterIntensity, forKey: kCIInputIntensityKey)
    }
    if inputKeys.contains(kCIInputRadiusKey) {
      currentFilter.setValue(filterRadius * 200, forKey: kCIInputRadiusKey)
    }
    if inputKeys.contains(kCIInputScaleKey) {
      currentFilter.setValue(filterScale * 10, forKey: kCIInputScaleKey)
    }

    guard let outputImage = currentFilter.outputImage else { return }

    if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
      let uiImage = UIImage(cgImage: cgimg)
      image = Image(uiImage: uiImage)
      processedImage = uiImage
    }

  }

  //------------------------------------------------------------------------
  // Set the CIFilter past into this function to the curretFilter state
  // property and then run the loadIMage method to process and filter
  // the image selected by the user with the photo picker.
  //------------------------------------------------------------------------
  func setFilter(_ filter: CIFilter) {
    currentFilter = filter
    loadImage()
  }

  func save() {
    guard let processedImage = processedImage else { return }

    // create an instance of ImageSaver class
    let imageSaver = ImageSaver()

    // specify the actions for the ImageSaver class error handler
    // colures.
    imageSaver.successHandler = {
      print("Image saved")
    }
    imageSaver.errorHanddler = {
      showingSaveError = true
      print("Error saving image: \($0.localizedDescription)")
    }

    imageSaver.writeToPhotoAlbum(image: processedImage)

  }

}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

2      

Nice! I didn't even know that was added.

Where can I find patch notes (or something similar) that would show what new features have been added to SwiftUI? Like this PhotosPicker or the new NavigationStack?

2      

I just did a Google serch to see if there might be a SwiftUI implementatin for the Photo Picker in iOS 16. I fould a few solutions already provided by other developers, the Apple Developer doc and the WWDC22 video "What's new in the Photos picker".

Google is your friend!!!!!

2      

@Bnerd  

I was wondering the same thing as @Fly0strich..how people identify what is included in an update..

2      

  1. Paul usually does a really good job of not just telling you what has been updated, but often giving sample code to demonstrate usage as well. Here's a recent one: What’s new in SwiftUI for iOS 16

  2. The Xcode release notes (e.g. for version 14) give a good rundown of what has changed in Swift.

  3. The release notes for the various operating systems go into detail about what is new in SwiftUI, as here for iOS 16.

  4. Paul maintains a github repo showing the SwiftUI interface files and their changelogs for each version of Xcode. These interfaces often contain useful notes and usage tips straight from Apple.

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.