In SwiftUI, when I execute the code below, the image data from the PhotoPicker does not pass into the CropView to customize. This happens every time I test this, but always with the first image only. If I cancel the (empty) crop view and try again, the second image is displayed properly.
import SwiftUI
import PhotosUI
struct Home: View {
@State private var showPicker: Bool = false
@State private var croppedImage: UIImage?
var body: some View {
NavigationStack{
VStack{
if let croppedImage{
Image(uiImage: croppedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 400)
} else{
Text("No Image is Selected")
.font(.caption)
.foregroundColor(.gray)
}
}
.navigationTitle("Crop Image Picker")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showPicker.toggle()
} label: {
Image(systemName: "photo.on.rectangle.angled")
.font(.callout)
}
.tint(.black)
}
}
.cropImagePicker(
options: [.circle],
show: $showPicker,
croppedImage: $croppedImage
)
}
}
}
extension View{
@ViewBuilder
func cropImagePicker(options: [Crop],show: Binding<Bool>,croppedImage: Binding<UIImage?>)->some View{
CustomImagePicker(options: options, show: show, croppedImage: croppedImage) {
self
}
}
@ViewBuilder
func frame(_ size: CGSize)->some View{
self
.frame(width: size.width, height: size.height)
}
func haptics(_ style: UIImpactFeedbackGenerator.FeedbackStyle){
UIImpactFeedbackGenerator(style: style).impactOccurred()
}
}
fileprivate struct CustomImagePicker<Content: View>: View {
var content: Content
var options: [Crop]
@Binding var show: Bool
@Binding var croppedImage: UIImage?
init(options: [Crop],show: Binding<Bool>,croppedImage: Binding<UIImage?>,@ViewBuilder content: @escaping ()->Content) {
self.content = content()
self._show = show
self._croppedImage = croppedImage
self.options = options
}
@State private var photosItem: PhotosPickerItem?
@State private var selectedImage: UIImage?
@State private var showDialog: Bool = false
@State private var selectedCropType: Crop = .circle
@State private var showCropView: Bool = false
var body: some View {
content
.photosPicker(isPresented: $show, selection: $photosItem)
.onChange(of: photosItem) { newValue in
if let newValue {
Task {
if let imageData = try? await newValue.loadTransferable(type: Data.self), let image = UIImage(data: imageData) {
await MainActor.run(body: {
selectedImage = image
selectedCropType = .circle
showCropView = true
})
}
}
}
}
.fullScreenCover(isPresented: $showCropView) {
selectedImage = nil
} content: {
CropView(image: selectedImage) { croppedImage, status in
if let croppedImage {
self.croppedImage = croppedImage
} else {
}
}
}
}
}
struct CropView: View {
var image: UIImage?
var onCrop: (UIImage?,Bool)->()
@Environment(.dismiss) private var dismiss
@State private var scale: CGFloat = 1
@State private var lastScale: CGFloat = 0
@State private var offset: CGSize = .zero
@State private var lastStoredOffset: CGSize = .zero
@GestureState private var isInteracting: Bool = false
var body: some View{
NavigationStack{
ImageView()
.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(Color.black, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.frame(maxWidth: .infinity,maxHeight: .infinity)
.navigationTitle("Profile Picture")
.navigationBarTitleDisplayMode(.inline)
.background {
Color.black
.ignoresSafeArea()
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
let renderer = ImageRenderer(content: ImageView(true))
renderer.proposedSize = .init(width: 300, height: 300)
renderer.scale = 10.0
if let image = renderer.uiImage{
onCrop(image,true)
}else{
onCrop(nil,false)
}
dismiss()
} label: {
Image(systemName: "checkmark")
.font(.callout)
.fontWeight(.semibold)
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Image(systemName: "xmark")
.font(.callout)
.fontWeight(.semibold)
}
}
}
}
}
@ViewBuilder
func ImageView(_ hideGrids: Bool = false) -> some View {
GeometryReader{
let size = $0.size
if let image{
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.overlay(content: {
GeometryReader { proxy in
let rect = proxy.frame(in: .named("CROPVIEW"))
Color.clear
.onChange(of: isInteracting) { newValue in
/// - true Dragging
/// - false Stopped Dragging
/// With the Help of GeometryReader
/// We can now read the minX,Y and maxX,Y of the Image
if !newValue{
withAnimation(.easeInOut(duration: 0.2)){
if rect.minX > 0{
/// - Resetting to Last Location
haptics(.medium)
offset.width = (offset.width - rect.minX)
}
if rect.minY > 0{
/// - Resetting to Last Location
haptics(.medium)
offset.height = (offset.height - rect.minY)
}
/// - Doing the Same for maxX,Y
if rect.maxX < size.width{
/// - Resetting to Last Location
haptics(.medium)
offset.width = (rect.minX - offset.width)
}
if rect.maxY < size.height{
/// - Resetting to Last Location
haptics(.medium)
offset.height = (rect.minY - offset.height)
}
}
/// - Storing Last Offset
lastStoredOffset = offset
}
}
}
})
.frame(size)
}
}
.scaleEffect(scale)
.offset(offset)
.overlay(content: {
if !hideGrids{
Grids()
}
})
.coordinateSpace(name: "CROPVIEW")
.gesture(
DragGesture()
.updating($isInteracting, body: { _, out, _ in
out = true
}).onChanged({ value in
let translation = value.translation
offset = CGSize(width: translation.width + lastStoredOffset.width, height: translation.height + lastStoredOffset.height)
})
)
.gesture(
MagnificationGesture()
.updating($isInteracting, body: { _, out, _ in
out = true
}).onChanged({ value in
let updatedScale = value + lastScale
/// - Limiting Beyond 1
scale = (updatedScale < 1 ? 1 : updatedScale)
}).onEnded({ value in
withAnimation(.easeInOut(duration: 0.2)){
if scale < 1{
scale = 1
lastScale = 0
}else{
lastScale = scale - 1
}
}
})
)
.frame(width: UIScreen.main.bounds.width * 0.9, height: UIScreen.main.bounds.width * 0.9)
.cornerRadius(1000)
}
@ViewBuilder
func Grids()->some View{
ZStack{
HStack{
ForEach(1...3,id: .self){_ in
Rectangle()
.fill(.white.opacity(0.35))
.frame(width: 0.8)
.frame(maxWidth: .infinity)
}
}
VStack{
ForEach(1...3,id: .self){_ in
Rectangle()
.fill(.white.opacity(0.35))
.frame(height: 0.8)
.frame(maxHeight: .infinity)
}
}
}
}
}
I thought this might be a race condition and so attempted to implement some kind of delay but the same results occurs.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
selectedCropType = .circle
showCropView = true
}