ImagePicker showing duplicate photos - ios

I am trying to understand why when I add a photo in my view it is duplicating the photo?
I want the user to add a photo to each card separately. I have tried to directly add selectedImage to my struct item image within the [Expense] array but xcode is screaming at me.
I know it has to with the #State selectedImage with its #Binding but Im not 100% sure how to enable this to work in my current scenario?
Here is the struct it is conforming to:
struct Card: Identifiable {
var id = UUID()
var title: String
var expenses: [Expense]
mutating func addExpenses() {
expenses.append(Expense(expensetype: "", amount: 0.0))
}
}
struct Expense: Identifiable {
var id = UUID()
var expensetype: String
var amount: Double = 0.0
var image: UIImage?
}
I am creating an array of cards on a button press in my contentview {
struct ContentView: View {
#State private var cards = [Card]()
#State private var sourceType: UIImagePickerController.SourceType = .photoLibrary
#State private var selectedImage: UIImage?
#State private var isImagePickerDisplay = false
#State private var showingOptions = false
var title = ""
var expensetype = ""
var amount: Double = 0.0
var image: UIImage?
var body: some View {
NavigationStack {
Form {
List {
Button("Add card") {
addCard()
}
ForEach($cards) { a in
Section {
TextField("Title", text: a.title)
Button("Add expense") {
a.wrappedValue.addExpenses()
}
ForEach(a.expenses) { b in
if a.expenses.isEmpty == false {
TextField("my expense", text: b.expensetype)
TextField("amount", value: b.amount, format: .number)
Button("Add image") {
withAnimation {
showingOptions = true
}
}
.confirmationDialog("", isPresented: $showingOptions, titleVisibility: .hidden) {
Button("Take photo") {
self.sourceType = .camera
self.isImagePickerDisplay.toggle()
}
Button("Choose photo") {
self.sourceType = .photoLibrary
self.isImagePickerDisplay.toggle()
}
}
if selectedImage != nil {
Image(uiImage: self.selectedImage!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 150, height: 150)
} else {
Image(systemName: "snow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 150, height: 150)
}
}
}
}
}
}
}
.sheet(isPresented: self.$isImagePickerDisplay) {
ImagePickerView(selectedImage: self.$selectedImage, sourceType: self.sourceType)
}
}
}
func addCard() {
cards.append(Card(title: title, expenses: []))
}
}
Here is my ImagePickerView
struct ImagePickerView: UIViewControllerRepresentable {
#Binding var selectedImage: UIImage?
#Environment(\.presentationMode) var isPresented
var sourceType: UIImagePickerController.SourceType
func makeUIViewController(context: Context) -> some UIViewController {
let imagePicker = UIImagePickerController()
imagePicker.sourceType = self.sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(picker: self)
}
}

Related

SwiftUI: Running into issues with Drag and Drop inside a LazyVGrid

I've been working on my own smart home app and have run into some issues when trying to build the grid for the app.
I've been basing this home app on this tutorial. The goal is that one can reorder the individually sized blocks in the grid basically like he or she wants. The blocks(items) represent different gadgets in the smart home application. The issue I'm facing is that I can't seem to get the drag & drop to work. Maybe it's better to put all the item views in one custom view and then run a "ForEach" loop for them so that the .onDrag works? I'm relatively new to SwiftUI so I appreciate every hint I can get this program to work.
Here is my code:
ItemModel1:
struct ItemModel: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}
ItemModel2:
struct ItemModel2: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel2 {
return ItemModel2(id: id, title: title)
}
}
It's essentially the same for the two other ItemModels 3 and 4..
ItemViewModel:
class ItemViewModel {
var items: [ItemModel] = []
#Published var currentGrid: ItemModel?
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "Item1"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
ContentView:
struct DropViewDelegate: DropDelegate {
var grid: ItemModel
var gridData: ItemViewModel
func performDrop(info: DropInfo) -> Bool {
return true
}
func dropEntered(info: DropInfo) {
let fromIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == gridData.currentGrid?.id
} ?? 0
let toIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == self.grid.id
} ?? 0
if fromIndex != toIndex{
withAnimation(.default){
let fromGrid = gridData.items[fromIndex]
gridData.items[fromIndex] = gridData.items[toIndex]
gridData.items[toIndex] = fromGrid
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}
struct ContentView: View {
#State var items: [ItemModel] = []
#State var items2: [ItemModel2] = []
#State var items3: [ItemModel3] = []
#State var items4: [ItemModel4] = []
#State var gridData = ItemViewModel()
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
let columns2 = [
GridItem(.flexible()),
]
var body: some View {
ZStack{
ScrollView{
VStack{
HStack(alignment: .top){
Button(action: saveButtonPressed, label: {
Text("Item1")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed2, label: {
Text("Item2")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed3, label: {
Text("Item3")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed4, label: {
Text("Item4")
.font(.title2)
.foregroundColor(.white)
})
}
LazyVGrid(
columns: columns,
alignment: .leading,
spacing: 12
){
ForEach(items) { item in
Item1View (item: item)
if 1 == 1 { Color.clear }
}
ForEach(items4) { item4 in
Item4View (item4: item4)
if 1 == 1 { Color.clear }
}
ForEach(items2) { item2 in
Item2View (item2: item2)
}
LazyVGrid(
columns: columns2,
alignment: .leading,
spacing: 12
){
ForEach(items3) { item3 in
Item3View (item3: item3)
}
}
}
.onDrag({
self.gridData = items
return NSItemProvider(item: nil, typeIdentifier:
self.grid)
})
.onDrop(of: [items], delegate: DropViewDelegate(grid:
items, gridData: gridData))
}
}
}
}
func saveButtonPressed() {
addItem(title: "Hello")
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func saveButtonPressed2() {
addItem2(title: "Hello")
}
func addItem2(title: String) {
let newItem = ItemModel2(title: title)
items2.append(newItem)
}
func saveButtonPressed3() {
addItem3(title: "Hello")
}
func addItem3(title: String) {
let newItem = ItemModel3(title: title)
items3.append(newItem)
}
func saveButtonPressed4() {
addItem4(title: "Hello")
}
func addItem4(title: String) {
let newItem = ItemModel4(title: title)
items4.append(newItem)
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
Item1:
struct Item1View: View {
#State var item: ItemModel
var body: some View {
HStack {
Text(item.title)
}
.padding( )
.frame(width: 396, height: 56)
.background(.black)
.cornerRadius(12.0)
}
}
Item2:
struct Item2View: View {
#State var item2: ItemModel2
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.black)
.cornerRadius(12.0)
}
}
Item3:
struct Item3View: View {
#State var item3: ItemModel3
var body: some View {
HStack {
Text(item3.title)
}
.padding( )
.frame(width: 182, height: 62)
.background(.black)
.cornerRadius(12.0)
}
}
Item4:
struct Item4View: View {
#State var item4: ItemModel4
var body: some View {
HStack {
Text(item4.title)
}
.padding( )
.frame(width: 396, height: 156)
.background(.black)
.cornerRadius(12.0)
}
}
I tried recreating the grid Asperi linked. However, the .onDrop doesn't seem to work like it should. The drop only occurs after you pressed another item to drag it. Only then will the previous items reorder themselves..
My version:
import SwiftUI
import UniformTypeIdentifiers
struct ItemModel6: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel6 {
return ItemModel6(id: id, title: title)
}
}
class Model: ObservableObject {
var data: [ItemModel6] = []
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
init() {
data = Array(repeating: ItemModel6(title: "title"), count:
100)
for i in 0..<data.count {
data[i] = ItemModel6(title: "Hello")
}
}
}
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var dragging: ItemModel6?
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns) {
ForEach(model.data) { item2 in GridItemView (item2:
item2)
.overlay(dragging?.id == item2.id ?
Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = item2
return NSItemProvider(object:
String(item2.id) as NSString)
}
.onDrop(of: [UTType.text], delegate:
DragRelocateDelegate(item: item2, listData: $model.data,
current: $dragging))
}
}.animation(.default, value: model.data)
}
.onDrop(of: [UTType.text], delegate:
DropOutsideDelegate(current: $dragging))
}
}
struct DropOutsideDelegate: DropDelegate {
#Binding var current: ItemModel6?
func performDrop(info: DropInfo) -> Bool {
current = nil
return true
}
}
struct DragRelocateDelegate: DropDelegate {
let item: ItemModel6
#Binding var listData: [ItemModel6]
#Binding var current: ItemModel6?
func dropEntered(info: DropInfo) {
if item != current {
let from = listData.firstIndex(of: current!)!
let to = listData.firstIndex(of: item)!
if listData[to].id != current!.id {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
self.current = nil
return true
}
}
struct GridItemView: View {
#State var item2: ItemModel6
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.gray)
.cornerRadius(12.0)
}
}
struct DemoDragRelocateView_Previews: PreviewProvider {
static var previews: some View {
DemoDragRelocateView()
.preferredColorScheme(.dark)
}
}

How to convert the image to base64 after take a photo from camera or library

I want to make a button that when pressed will take a photo, after I take a photo I can directly convert the photo to base64 and will be used to post to the API
This is my code :)
import SwiftUI
import UIKit
struct ImagePicker: UIViewControllerRepresentable {
#Binding var selectedImage: UIImage
#Environment(\.presentationMode) private var presentationMode
var sourceType: UIImagePickerController.SourceType = .photoLibrary
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: Context) {
//leave alone for right now
}
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.selectedImage = image
}
parent.presentationMode.wrappedValue.dismiss()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
class ImageConverter {
func base64ToImage(_ base64String: String) -> UIImage? {
guard let imageData = Data(base64Encoded: base64String) else { return nil }
return UIImage(data: imageData)
}
func imageToBase64(_ image: UIImage) -> String? {
return image.jpegData(compressionQuality: 1)?.base64EncodedString()
}
}
this is some of my view, i want when i press the button it will direct me to the camera and when i take a photo it will convert it to base64 and save it in a string
import SwiftUI
struct ProfileView: View {
let imageManager = ImageConverter()
#State var changeProfileImage = false
#State var openCameraRoll = false
#State var imageSelected = UIImage()
var body: some View {
ZStack(alignment: .bottomTrailing) {
Button(action: {
changeProfileImage = true
openCameraRoll = true
}, label: {
if changeProfileImage {
Image(uiImage: imageSelected)
.profileImageMod()
} else {
Image("AddProfileImage")
.profileImageMod()
}
})
Image(systemName: "plus")
.frame(width: 30, height: 30)
.foregroundColor(.white)
.background(Color.gray)
.clipShape(Circle())
}.sheet(isPresented: $openCameraRoll) {
ImagePicker(selectedImage: $imageSelected, sourceType: .camera)
}.onAppear {
}
}
}
UPDATE this is my Imagepicker
import SwiftUI
import UIKit
struct ImagePicker: UIViewControllerRepresentable {
#Binding var selectedImage: UIImage
#Environment(\.presentationMode) private var presentationMode
var sourceType: UIImagePickerController.SourceType = .photoLibrary
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: Context) {
//leave alone for right now
}
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.selectedImage = image
}
parent.presentationMode.wrappedValue.dismiss()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
class ImageConverter {
func base64ToImage(_ base64String: String) -> UIImage? {
guard let imageData = Data(base64Encoded: base64String) else { return nil }
return UIImage(data: imageData)
}
func imageToBase64(_ image: UIImage) -> String? {
return image.jpegData(compressionQuality: 1)?.base64EncodedString()
}
}
you could use something like this approach, using the .onDismiss()
to then convert your imageSelected to base64 string using your ImageConverter.
In ProfileView,
.sheet(isPresented: $openCameraRoll, onDismiss: didDismiss) {
ImagePicker(selectedImage: $imageSelected, sourceType: .camera)
}
func didDismiss() {
let b64Str = imageManager.imageToBase64(imageSelected)
print("\n---> b64Str: \(b64Str?.count) \n")
}
Also remove the ZStack, or replace it by a VStack
EDIT-1: here some example code that works for me:
struct ProfileView: View {
let imageManager = ImageConverter()
#State var changeProfileImage = false
#State var openCameraRoll = false
#State var imageSelected = UIImage()
var body: some View {
VStack {
Button(action: {
openCameraRoll = true
}, label: {
Image(systemName: "plus")
// .profileImageMod()
})
if changeProfileImage {
Image(uiImage: imageSelected)
.resizable() // <-- here
.frame(width: 222, height: 222)
.foregroundColor(.white)
.background(Color.gray)
.clipShape(Circle())
} else {
Image(systemName: "questionmark")
.frame(width: 222, height: 222)
.foregroundColor(.white)
.background(Color.gray)
.clipShape(Circle())
}
}
.sheet(isPresented: $openCameraRoll, onDismiss: didDismiss) {
ImagePicker(selectedImage: $imageSelected, sourceType: .camera)
}
}
func didDismiss() {
changeProfileImage = true
let b64Str = imageManager.imageToBase64(imageSelected)
print("\n---> b64Str: \(b64Str?.count) \n")
}
}

How to handle loading screens in SwiftUI?

I am trying to implement a simple 3-screen application on SwiftUI. The 1st screen allows the user to select a photo, the 2nd must show the loading screen while the photo is analyzed, and the 3rd shows analysis results. Currently, my application doesn't show the loading screen at all and just jumps to the 3rd screen.
ContentView.swift:
struct ContentView: View {
#State var image: Image = Image("MyImageName")
#State var tiles: [Tile] = []
#State var isSelectionView: Bool = true
#State var isLoadingView: Bool = false
#State var isAdjustmentView: Bool = false
var body: some View {
if (isSelectionView) {
SelectionView(image: $image, isSelectionView: $isSelectionView, isLoadingView: $isLoadingView)
}
if (isLoadingView) {
LoadingMLView(image: $image, tiles: $tiles, isLoadingView: $isLoadingView, isAdjustmentView: $isAdjustmentView)
}
if (isAdjustmentView) {
AdjustmentView(tiles: $tiles)
}
}}
SelectionView.swift:
struct SelectionView: View {
#State var showCaptureImageView = false
#State var showPopover = false
#State var sourceType: UIImagePickerController.SourceType = .camera
#State var imageSelected = false
#Binding var image: Image
#Binding var isSelectionView: Bool
#Binding var isLoadingView: Bool
var body: some View {
VStack() {
if (!showCaptureImageView && !imageSelected) {
Spacer()
Button(action: {
showPopover.toggle()
}) {
Text("Choose photo")
.frame(width: 100, height: 100)
.foregroundColor(Color.white)
.background(Color.blue)
.clipShape(Circle())
}
.confirmationDialog("Select location", isPresented: $showPopover) {
Button("Camera") {
sourceType = .camera
showPopover.toggle()
showCaptureImageView.toggle()
}
Button("Photos") {
sourceType = .photoLibrary
showPopover.toggle()
showCaptureImageView.toggle()
}
}
} else if (showCaptureImageView) {
CaptureImageView(isShown: $showCaptureImageView, image: $image, sourceType: $sourceType)
} else {
ActivityIndicator()
.frame(width: 200, height: 200)
.foregroundColor(.blue)
}
}
.onChange(of: image) { image in
showCaptureImageView.toggle()
imageSelected.toggle()
isSelectionView.toggle()
isLoadingView.toggle()
}
}}
LoadingMLView.swift:
struct LoadingMLView: View {
#Binding var image: Image
#Binding var tiles: [Tile]
#Binding var isLoadingView: Bool
#Binding var isAdjustmentView: Bool
var body: some View {
ActivityIndicator()
.frame(width: 200, height: 200)
.foregroundColor(.blue)
.onAppear {
do {
let model = try VNCoreMLModel(for: MyModel().model)
let request = VNCoreMLRequest(model: model, completionHandler: myResultsMethod)
let uiImage: UIImage = image.asUIImage()
guard let ciImage = CIImage(image: uiImage) else {
fatalError("Unable to create \(CIImage.self) from \(image).")
}
let handler = VNImageRequestHandler(ciImage: ciImage)
do {
try handler.perform([request])
} catch {
print("Something went wrong!")
}
func myResultsMethod(request: VNRequest, error: Error?) {
guard let results = request.results as? [VNRecognizedObjectObservation]
else { fatalError("error") }
for result in results {
let tile = Tile(name: result.labels[0].identifier)
tiles.append(tile)
}
isLoadingView.toggle()
isAdjustmentView.toggle()
}
} catch {
print("Error:", error)
}
}
}}
No matter how I change the toggle between screens, it just doesn't seem reactive and the views don't switch momentarily. What am I doing wrong? Is there a proper way to show loading screen right after the image is selected?
Seems like you're missing an else to avoid rendering the screens you don't want:
var body: some View {
if (isSelectionView) {
SelectionView(image: $image, isSelectionView: $isSelectionView, isLoadingView: $isLoadingView)
} else if (isLoadingView) {
LoadingMLView(image: $image, tiles: $tiles, isLoadingView: $isLoadingView, isAdjustmentView: $isAdjustmentView)
} else if (isAdjustmentView) {
AdjustmentView(tiles: $tiles)
}
}}
But, in general what you want is also called "state machine" - essentially your app can be in one of 3 possible states: selection, loading, adjustment, so you should have an enum #State to represent the current state, instead of the 3 bool values:
struct ContentView: View {
// ...
enum AppState { case selection, loading, adjustment }
#State private var state = AppState.selection
var body: some View {
switch state {
case .selection:
SelectionView(image: $image, state: $state)
case .loading:
LoadingMLView(image: $image, tiles: $tiles, state: $state)
case .adjustment:
AdjustmentView(tiles: $tiles)
}
}
When you want to switch to the next screen, just change state value...
Turns out, the main performance issue was caused by the image type conversion:
let uiImage: UIImage = image.asUIImage()
After passing the image as UIImage in the first place, the app started working fast, so there was no need to handle the loading in the first place.

SwiftUI LazyVStack overlapping images

I am trying to display two columns of images in a LazyVStack embedded in a scroll view however the the second row of images partially overlaps the row above. I'm not sure if this is an issue with the LazyVStack itself or an issue with the Photo.swift view.
The output looks like this
The two view files
ContentView.swift
struct ContentView: View {
#State private var image: Image?
#State private var showingCustomCamera = false
#State private var inputImage: UIImage?
#State private var photos: [UIImage] = []
func addImageToArray() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
let ciImage = CIImage(cgImage: inputImage.cgImage!)
let options = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: options)!
let faces = faceDetector.features(in: ciImage)
if let face = faces.first as? CIFaceFeature {
print("Found face at \(face.bounds)")
print(face.faceAngle)
print(face.hasSmile)
print(face.leftEyeClosed)
print(face.rightEyeClosed)
if face.leftEyeClosed {
print("Left Eye Closed \(face.leftEyePosition)")
}
if face.rightEyeClosed {
print("Right Eye Closed \(face.rightEyePosition)")
}
if face.hasSmile {
print("Person is smiling \(face.mouthPosition)")
}
}
photos.append(inputImage)
}
let columns = [
GridItem(.flexible(), spacing: 20),
GridItem(.flexible(), spacing: 20)
]
var body: some View {
NavigationView {
VStack{
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
AddPhoto(showCamera: $showingCustomCamera)
ForEach(photos, id: \.self) { photo in
PassportPhoto(img: photo)
}
}
.padding()
}
HStack {
Button(action: {
//
}, label: {
Image(systemName: "printer.fill.and.paper.fill")
Text("Print")
})
.padding()
.foregroundColor(.primary)
Button(action: {
//
}, label: {
Image(systemName: "externaldrive.fill.badge.icloud")
Text("Digital Upload")
})
.padding()
.foregroundColor(.primary)
}
}
.sheet(isPresented: $showingCustomCamera, onDismiss: addImageToArray) {
CustomCameraView(image: self.$inputImage)
}
.navigationTitle("Add Photos")
}
}
}
Photo.swift
struct Photo: View {
var img: UIImage
#State private var overlay: Bool = false
var body: some View {
GeometryReader { geometry in
VStack {
ZStack(alignment: .top) {
Image(uiImage: img)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: geometry.size.width * 1.29, alignment: .top)
.clipped()
.cornerRadius(10)
.onTapGesture {
self.overlay.toggle()
}
if overlay {
Template()
}
}
}
}
}
}
Anyone have any idea? I feel like I'm missing something obvious.
CustomCameraView.swift (as requested)
import SwiftUI
import AVFoundation
struct CustomCameraView: View {
#Binding var image: UIImage?
#State var didTapCapture: Bool = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack(alignment: .center) {
CustomCameraRepresentable(image: self.$image, didTapCapture: $didTapCapture)
.overlay(Template(),alignment: .center)
.overlay(
CaptureButtonView().onTapGesture {
self.didTapCapture = true
}
, alignment: .bottom)
.overlay(
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "multiply")
.scaleEffect(2)
.padding(20)
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
})
.foregroundColor(.white)
.padding()
, alignment: .topTrailing)
}
}
}
struct CustomCameraRepresentable: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var image: UIImage?
#Binding var didTapCapture: Bool
func makeUIViewController(context: Context) -> CustomCameraController {
let controller = CustomCameraController()
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ cameraViewController: CustomCameraController, context: Context) {
if(self.didTapCapture) {
cameraViewController.didTapRecord()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
let parent: CustomCameraRepresentable
init(_ parent: CustomCameraRepresentable) {
self.parent = parent
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
parent.didTapCapture = false
if let imageData = photo.fileDataRepresentation() {
parent.image = UIImage(data: imageData)
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
class CustomCameraController: UIViewController {
var image: UIImage?
var captureSession = AVCaptureSession()
var backCamera: AVCaptureDevice?
var frontCamera: AVCaptureDevice?
var currentCamera: AVCaptureDevice?
var photoOutput: AVCapturePhotoOutput?
var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
//DELEGATE
var delegate: AVCapturePhotoCaptureDelegate?
func didTapRecord() {
let settings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: settings, delegate: delegate!)
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
setupCaptureSession()
setupDevice()
setupInputOutput()
setupPreviewLayer()
startRunningCaptureSession()
}
func setupCaptureSession() {
captureSession.sessionPreset = AVCaptureSession.Preset.photo
}
func setupDevice() {
let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera],
mediaType: AVMediaType.video,
position: AVCaptureDevice.Position.unspecified)
for device in deviceDiscoverySession.devices {
switch device.position {
case AVCaptureDevice.Position.front:
self.frontCamera = device
case AVCaptureDevice.Position.back:
self.backCamera = device
default:
break
}
}
self.currentCamera = self.backCamera
}
func setupInputOutput() {
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: currentCamera!)
captureSession.addInput(captureDeviceInput)
photoOutput = AVCapturePhotoOutput()
photoOutput?.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])], completionHandler: nil)
captureSession.addOutput(photoOutput!)
} catch {
print(error)
}
}
func setupPreviewLayer()
{
let rect = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width * 1.29)
self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait
self.cameraPreviewLayer?.frame = rect
self.view.layer.insertSublayer(cameraPreviewLayer!, at: 0)
}
func startRunningCaptureSession(){
captureSession.startRunning()
}
}
struct CaptureButtonView: View {
#State private var animationAmount: CGFloat = 1
var body: some View {
Image(systemName: "camera").font(.largeTitle)
.padding(30)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.red)
.scaleEffect(animationAmount)
.opacity(Double(2 - animationAmount))
.animation(Animation.easeOut(duration: 1)
.repeatForever(autoreverses: false))
)
.padding(.bottom)
.onAppear
{
self.animationAmount = 2
}
}
}
You shouldn’t use GeometryReader from within the ScrollView, it will create all sort of mess for you. Instead define it at top level just under VStack, and pass the proxy down to Photo view to set Frame.
Check the code below-:
import SwiftUI
struct Test1: View {
#State private var image: Image?
#State private var showingCustomCamera = false
#State private var inputImage: UIImage?
#State private var photos: [UIImage] = []
func addImageToArray() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
let ciImage = CIImage(cgImage: inputImage.cgImage!)
let options = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: options)!
let faces = faceDetector.features(in: ciImage)
if let face = faces.first as? CIFaceFeature {
print("Found face at \(face.bounds)")
print(face.faceAngle)
print(face.hasSmile)
print(face.leftEyeClosed)
print(face.rightEyeClosed)
if face.leftEyeClosed {
print("Left Eye Closed \(face.leftEyePosition)")
}
if face.rightEyeClosed {
print("Right Eye Closed \(face.rightEyePosition)")
}
if face.hasSmile {
print("Person is smiling \(face.mouthPosition)")
}
}
photos.append(inputImage)
}
let columns =
[GridItem(.flexible(),spacing: 20),
GridItem(.flexible(),spacing: 20)]
var body: some View {
NavigationView {
VStack{
GeometryReader { geometry in
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
// AddPhoto(showCamera: $showingCustomCamera) // Uncomment in your case
ForEach(0..<50, id: \.self) { photo in
Photo(img: "ABC", proxy: geometry) // Pass photo as you were doing
}
}
.padding()
}
}
HStack {
Button(action: {
//
}, label: {
Image(systemName: "printer.fill.and.paper.fill")
Text("Print")
})
.padding()
.foregroundColor(.primary)
Button(action: {
//
}, label: {
Image(systemName: "externaldrive.fill.badge.icloud")
Text("Digital Upload")
})
.padding()
.foregroundColor(.primary)
}
}
.sheet(isPresented: $showingCustomCamera, onDismiss: addImageToArray) {
// CustomCameraView(image: self.$inputImage)
}
.navigationTitle("Add Photos")
}
}
}
struct Photo: View {
var img: String
var proxy:GeometryProxy
#State private var overlay: Bool = false
var body: some View {
// GeometryReader { geometry in
VStack {
ZStack(alignment: .top) {
Image(img)
.resizable()
.aspectRatio(contentMode: .fill)
// .frame(width: 170, height: 200)
.frame(width: proxy.size.width * 0.4, height: proxy.size.width * 0.5, alignment: .top)
.clipped()
.cornerRadius(10)
.onTapGesture {
self.overlay.toggle()
}
if overlay {
// Template()
}
}
}
//}
}
}

SwiftUI: How to select multi items(image) with ForEach?

I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.

Resources