Image Picker — SwiftUI

Umut Boz
KoçSistem
Published in
7 min readJun 13, 2024

In this article, you will find a Image Picker example with a custom design within the SwiftUI framework. This method customizes the Image Picker specifically designed for your application. And You find a specially designed Image Picker that works in conjunction with the custom segmented control in the previous article.

We discussed custom segmented control in the previous article. In this study, we will continue as using with this segmented control.

➡️

Image Picker

Basically, the Image Picker is the view that allows you to select one or more assets from your photo library. It able to provides access to the camera due to new image content. Performs management on both camera and photo library.

Using ImagePicker on SwiftUI

Old and new methods can be used. PhotosPicker, a UIKit method or SwiftUI solution, may be preferred.

Since the minimum SDK for the running application is 15, our choice will be the UIKit solution.

You can find information about the PhotosPicker solution below.

PhotosPicker

In iOS 16, Apple introduced PhotosPicker to SwiftUI that it has the same functionalities as its UIKit counterpart. If your app will only support device running iOS 16 or up.

PhotosPicker has a include set of features for users to view and select assets, such as images and videos.

  • SwiftUI exclusive content
  • Availability for iPad, macOS and watchOS.

Working with Customize ImagePicker

We start to integrate the UIImagePickerController view and the protocol of UIViewControllerRepresentable which are UIKit solutions into SwiftUI project.

As you can see below, ImagePickerItem are the elements kept in the ImagePicker. As you can see below, ImagePickerItem are the elements kept in the image Picker which this is to be used as property

Custom Image Picker


struct ImagePickerItem{
var isRepetitive: Bool = false
var image : UIImage
var borderUIColor : UIColor = UIColor.red

//If the same image is added more than once, we draw a border on the image.
mutating func clearBorder(){
self.isRepetitive = false
}
}

struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) private var presentationMode
var sourceType: UIImagePickerController.SourceType = .photoLibrary
@Binding var images : [ImagePickerItem]
@Binding var tag : Int
@Binding var shouldSelectedImageCount : Int


func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.allowsEditing = false
imagePicker.sourceType = sourceType
imagePicker.delegate = context.coordinator

return imagePicker
}

func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {

}

func makeCoordinator() -> Coordinator {
Coordinator(self)
}

final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

var parent: ImagePicker

init(_ parent: ImagePicker) {
self.parent = parent
}

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
parent.images[parent.tag].image = image
/* it is doing to control that Has Same Image our picker
Task{
do{
try await parent.handleImages()
}catch{
print("error handleImages")
}
}
*/
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}

There are four parameters that we will get from outside.

// sourceType : .camara or photo
// images : A two-way binding list where we will keep the selections
// tag : last selected index
// shouldSelectedImageCount: How many choices do we want?

Representation of Items in Image Picker

We need a container view that organizes it in a vertically grows grid.

A grid that grows downward according to a specified number of images. LazyVGrid will be used for this which is ui member of SwiftUI

LazyVGrid(columns: columns, spacing: 10){
ForEach(0 ..< viewModel.selectedImages.count, id: \.self){ number in
ZStack{
// your picker design
}
}
}

Picker UI View

AnyView
AnyView(Image(uiImage: viewModel.selectedImages[number].image)
.resizable()
.frame(width: 96, height: 96)
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
.modifier(ConditionalAddCircleBorderModifier(isRepetitive: viewModel.selectedImages[number].isRepetitive, borderColor: viewModel.selectedImages[number].borderUIColor))
.pressAction{
lastSelectedIndex = number
print(number)
showSheet = true
}onRelease: {
})
.sheet(isPresented: $showSheet) {
ImagePicker(sourceType: sourceType, images: Binding<[ImagePickerItem]>(
get: { viewModel.selectedImages },
set: { viewModel.selectedImages = $0 }
), tag: Binding<Int>(
get: { lastSelectedIndex },
set: { lastSelectedIndex = $0 }
),shouldSelectedImageCount: Binding<Int>(
get: { shouldSelectImageCount },
set: { shouldSelectImageCount = $0 }
))
}

When Our designed view is pressed, pressAction is running and showSheet is setting true flag and the selected index is saved. I will talk about modifier later.

In Sheet display, our Image Picker is started, if the source is camera, the camera source is shown, if it is photo library, the photos are shown.

In the last case, there is the following code so as to design of Image Picker

 LazyVGrid(columns: columns, spacing: 10){
ForEach(0 ..< viewModel.selectedImages.count, id: \.self){ number in
ZStack{
AnyView(Image(uiImage: viewModel.selectedImages[number].image)
.resizable()
.frame(width: 96, height: 96)
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
.modifier(
ConditionalAddCircleBorderModifier
(
isRepetitive: viewModel.selectedImages[number].isRepetitive,
borderColor: viewModel.selectedImages[number].borderUIColor
)
)
.pressAction{
lastSelectedIndex = number
print(number)
showSheet = true
}onRelease: {
})
.sheet(isPresented: $showSheet) {
ImagePicker(sourceType: sourceType, images: Binding<[ImagePickerItem]>(
get: { viewModel.selectedImages },
set: { viewModel.selectedImages = $0 }
), tag: Binding<Int>(
get: { lastSelectedIndex },
set: { lastSelectedIndex = $0 }
),shouldSelectedImageCount: Binding<Int>(
get: { shouldSelectImageCount },
set: { shouldSelectImageCount = $0 }
))
//AnyView}

//ZStack }
// foreach }
//LazyVGrid}

Could I have chosen the same picture?

it is possible. There is not any control for to this. If done, you can provide a representation like below.👇

Requirement List

  • extension of Same image control
  • grouping the same images
  • modifier
  • asynchronous programming

Extension of Same image Control,

We can find out whether there are the same images in a group from the image length. For this, we write an extension to the image list.

extension Array<UIImage> {
func toLens() -> ([Int], [Int:Int]){
let lens = self.map{ $0.data().count }
var indicies = [Int:Int]()
let set = Set(lens)
if set.count < lens.count{
let setArray = set.map { $0 }
let rst = lens - setArray
for (ji,jm) in rst.enumerated(){
for (ki,km) in lens.enumerated(){
if jm == km{
indicies[ki] = km
}
}
}
}
return (lens, indicies)
}

}

This tells us if there are more than one of the same images as a group dictionary.

Grouping the Same Images

According to this data, we capture similar images in the image picker.

  func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
parent.images[parent.tag].image = image
Task{
do{
try await parent.handleImages()
}catch{
print("error handleImages")
}
}
}
parent.presentationMode.wrappedValue.dismiss()
}
}

@MainActor func handleImages() async throws -> Void{
let colors = [UIColor.red, UIColor.green, UIColor.yellow, UIColor.orange]
let (sizeList,sizeDictionary) = images.map{$0.image}.toLens()
let lensGrouping = Dictionary(grouping: sizeDictionary) { $0.value }

// clear before Border
for index in 0 ..< images.count{
images[index].clearBorder()
}

var repretitiveDetectedIndex = [Int]()
// draw border same images
for (groupIndex,group) in lensGrouping.enumerated(){
for value in group.value{
//let image = images[value.key]
let index = value.key
if value.value != 5107{
repretitiveDetectedIndex.append(index)
images[index].isRepetitive = true
print("groupIndex : \(groupIndex)")
images[index].borderUIColor = colors[groupIndex]
}else{

}
//let relatedIndex = group.value.map{$0.0}.filter{$0 != value.key}[0]
//print(group)
}
}
var nonCompletedMembers = [Int]()
for (index,member) in sizeList.enumerated(){
if member == 5107{
//is avatar default Icon
nonCompletedMembers.append(index)
}
}

self.shouldSelectedImageCount = nonCompletedMembers.count + repretitiveDetectedIndex.count / 2

}

Within the data, that image is repeatedly assigned the index of the signs and the group it belongs to.

images[index].isRepetitive = true
print("groupIndex : \(groupIndex)")
images[index].borderUIColor = colors[groupIndex]

Modifier

The modifier takes these repeated images and draws the ones in its group with the same color.

.modifier(
ConditionalAddCircleBorderModifier(
isRepetitive: viewModel.selectedImages[number].isRepetitive,
borderColor: viewModel.selectedImages[number].borderUIColor
)
)
struct ConditionalAddCircleBorderModifier: ViewModifier {

var isRepetitive: Bool
var borderColor: UIColor

@ViewBuilder
func body(content: Content) -> some View {
if self.isRepetitive {
content.overlay(BottomBorder().stroke(Color(uiColor: borderColor).opacity(0.8), lineWidth: 1))
}else{
content
}
}
}

struct BottomBorder: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: CGPoint(x: 48, y:48), radius: 50, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true)

return path.strokedPath(.init(lineWidth: 1, dash: [5, 3], dashPhase: 10))
}
}

Asynchronous Programming

The dimensions available in the image control can be very large thus ensuring asynchronous processing performance.

Task{
do{
try await parent.handleImages()
}catch{
print("error handleImages")
}
}

--

--