Initial Commit

This commit is contained in:
aminecmi
2024-07-26 15:16:48 +02:00
parent 656680d19f
commit 9f51394788
20 changed files with 1135 additions and 0 deletions

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "Frame 1(2).png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,16 @@
//
// Extension.swift
// Swipe That Pic
//
// Created by Amine Bou on 27/07/2024.
//
import Foundation
import Photos
extension Array {
func random() -> Element {
let randomIndex = Int(arc4random()) % self.count
return self[randomIndex]
}
}

View File

@ -0,0 +1,17 @@
//
// Item.swift
// Swipe That Pic
//
// Created by Amine Bou on 26/07/2024.
//
import Foundation
import SwiftData
@Model
final class Item {
@Attribute(.unique) var localIdentfier: String
init(localIdentfier: String) {
self.localIdentfier = localIdentfier
}
}

View File

@ -0,0 +1,98 @@
//
// PhotosService.swift
// Swipe That Pic
//
// Created by Amine Bou on 27/07/2024.
//
import Foundation
import Photos
import UIKit
import SwiftData
import SwiftUI
class PhotosService: ObservableObject {
var notKept: PHFetchResult<PHAsset> = PHFetchResult()
@Published var one: PHAsset?
var modelContainer: ModelContainer
init(modelContainer: ModelContainer) {
self.modelContainer = modelContainer
}
var imageCachingManager = PHCachingImageManager()
@MainActor func fetchNotKeptPhotos() {
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
switch status {
case .authorized:
self.fetchNotKeptPhotosAuthorized()
break;
@unknown default:
fatalError()
}
}
}
@MainActor func fetchNotKeptPhotosAuthorized() {
imageCachingManager.allowsCachingHighQualityImages = false
let fetchOptions = PHFetchOptions()
fetchOptions.includeHiddenAssets = false
do {
let items = try self.modelContainer.mainContext.fetch(FetchDescriptor<Item>())
if (items.count > 0) {
fetchOptions.predicate = NSPredicate(format: "NOT localIdentifier IN %@",
argumentArray: [items.map {$0.localIdentfier}])
}
} catch {
print("Can't get model")
}
fetchOptions.sortDescriptors = [
NSSortDescriptor(key: "creationDate", ascending: false)
]
DispatchQueue.main.async {
self.notKept = PHAsset.fetchAssets(with: .image, options: fetchOptions)
if (self.notKept.count > 0) {
self.one = self.notKept.object(at: Int.random(in: 0..<self.notKept.count))
} else {
self.one = nil
}
}
}
func fetchImage(
byLocalIdentifier localId: String,
targetSize: CGSize = PHImageManagerMaximumSize,
contentMode: PHImageContentMode = .default
) async throws -> UIImage? {
let results = PHAsset.fetchAssets(
withLocalIdentifiers: [localId],
options: nil
)
guard let asset = results.firstObject else {
fatalError("No asset")
}
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.resizeMode = .fast
options.isNetworkAccessAllowed = true
options.isSynchronous = true
return try await withCheckedThrowingContinuation { [weak self] continuation in
/// Use the imageCachingManager to fetch the image
self?.imageCachingManager.requestImage(
for: asset,
targetSize: targetSize,
contentMode: contentMode,
options: options,
resultHandler: { image, info in
/// image is of type UIImage
if let error = info?[PHImageErrorKey] as? Error {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: image)
}
)
}
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,34 @@
//
// Swipe_That_PicApp.swift
// Swipe That Pic
//
// Created by Amine Bou on 26/07/2024.
//
import SwiftUI
import SwiftData
import Photos
@main
struct Swipe_That_PicApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(PhotosService(modelContainer: sharedModelContainer))
}
.modelContainer(sharedModelContainer)
}
}

View File

@ -0,0 +1,79 @@
//
// PhotoTumbnailView.swift
// Swipe That Pic
//
// Created by Amine Bou on 27/07/2024.
//
import Foundation
import Photos
import SwiftUI
struct BigPicture: View {
private var image: Image
@State var scale = 1.0
@State var lastScale = 0.0
@State var offset: CGSize = .zero
@State var lastOffset: CGSize = .zero
init(image: Image) {
self.image = image
}
var body: some View {
GeometryReader { proxy in
image
.resizable()
.scaledToFill()
.scaleEffect(scale)
.offset(offset)
.frame(width: proxy.size.width, height: proxy.size.height)
.gesture(
MagnificationGesture(minimumScaleDelta: 0)
.onChanged({ value in
withAnimation(.interactiveSpring()) {
scale = handleScaleChange(value)
}
})
.onEnded({ _ in
lastScale = scale
}).simultaneously(
with: DragGesture(minimumDistance: 0)
.onChanged({ value in
withAnimation(.interactiveSpring()) {
offset = handleOffsetChange(value.translation)
}
})
.onEnded({ _ in
lastOffset = offset
})
).simultaneously(with: TapGesture(count: 2).onEnded({ Void in
scale = 1.0
lastScale = 0.0
offset = .zero
lastOffset = .zero
}))
)
}
// We'll also make sure that the photo will
// be square
.aspectRatio(1, contentMode: .fit)
}
private func handleScaleChange(_ zoom: CGFloat) -> CGFloat {
lastScale + zoom - (lastScale == 0 ? 0 : 1)
}
private func handleOffsetChange(_ offset: CGSize) -> CGSize {
var newOffset: CGSize = .zero
newOffset.width = offset.width + lastOffset.width
newOffset.height = offset.height + lastOffset.height
return newOffset
}
}

View File

@ -0,0 +1,70 @@
//
// ContentView.swift
// Swipe That Pic
//
// Created by Amine Bou on 26/07/2024.
//
import SwiftUI
import SwiftData
import Photos
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@EnvironmentObject var photoLibraryService: PhotosService
@Query private var items: [Item]
var body: some View {
NavigationSplitView {
VStack {
PhotoThumbnailView(asset: $photoLibraryService.one)
Spacer()
HStack {
Button(role: .destructive, action: {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.deleteAssets([photoLibraryService.one as Any] as NSArray)
}, completionHandler: { success, error in
if success {
photoLibraryService.fetchNotKeptPhotos()
} else {
}})
}) {
Label("Supprimer", systemImage: "minus.circle.fill")
}.buttonStyle(.borderedProminent)
Spacer()
Button(action: {
let newItem = Item(localIdentfier: photoLibraryService.one!.localIdentifier)
modelContext.insert(newItem)
photoLibraryService.fetchNotKeptPhotos()
}) {
Label("Garder", systemImage: "plus.circle.fill")
}.buttonStyle(.borderedProminent)
}.disabled(photoLibraryService.one == nil)
}
.toolbar {
if (items.count > 0) {
ToolbarItem {
NavigationLink(destination: IgnoredPictures()) {
Label("Handle kept", systemImage: "tray.and.arrow.up.fill")
}
}
}
}
} detail: {
Text("Select an item")
}.onAppear(perform: {
photoLibraryService.fetchNotKeptPhotos()
})
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
}

View File

@ -0,0 +1,53 @@
//
// ContentView.swift
// Swipe That Pic
//
// Created by Amine Bou on 26/07/2024.
//
import SwiftUI
import SwiftData
import Photos
struct IgnoredPictures: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
var body: some View {
NavigationSplitView {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200)), GridItem(.adaptive(minimum: 200))], alignment: .leading) {
ForEach(items) { item in
PhotoThumbnailFromStringView(localIdentifier: item.localIdentfier)
}
}
.toolbar {
ToolbarItem {
Button(action: clearItems) {
Label("Clear", systemImage: "bubbles.and.sparkles.fill")
}
}
}
}
} detail : {
Text("Ignored pictures")
}
}
private func clearItems() {
withAnimation {
do {
try modelContext.delete(model: Item.self)
} catch {
print("did not work")
}
}
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
}

View File

@ -0,0 +1,73 @@
//
// PhotoTumbnailView.swift
// Swipe That Pic
//
// Created by Amine Bou on 27/07/2024.
//
import Foundation
import Photos
import SwiftUI
struct PhotoThumbnailFromStringView: View {
@EnvironmentObject var photoLibraryService: PhotosService
@State private var image: Image?
@State var localIdentifier: String?
func loadImageAsset(
targetSize: CGSize = PHImageManagerMaximumSize
) async {
if (localIdentifier != nil) {
guard let uiImage = try? await photoLibraryService
.fetchImage(
byLocalIdentifier: localIdentifier!,
targetSize: targetSize
) else {
image = nil
return
}
image = Image(uiImage: uiImage)
} else {
image = nil
}
}
var body: some View {
ZStack {
// Show the image if it's available
if let image = image {
GeometryReader { proxy in
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(
width: proxy.size.width,
height: proxy.size.width
)
.clipped()
}
// We'll also make sure that the photo will
// be square
.aspectRatio(1, contentMode: .fit)
} else {
// Otherwise, show a gray rectangle with a
// spinning progress view
Rectangle()
.foregroundColor(.gray)
.aspectRatio(1, contentMode: .fit)
ProgressView()
}
}
// We need to use the task to work on a concurrent request to
// load the image from the photo library service, which
// is asynchronous work.
.task(id: localIdentifier) {
await loadImageAsset()
}
// Finally, when the view disappears, we need to free it
// up from the memory
.onDisappear {
image = nil
}
}
}

View File

@ -0,0 +1,73 @@
//
// PhotoTumbnailView.swift
// Swipe That Pic
//
// Created by Amine Bou on 27/07/2024.
//
import Foundation
import Photos
import SwiftUI
struct PhotoThumbnailView: View {
@EnvironmentObject var photoLibraryService: PhotosService
@State private var image: Image?
@Binding var asset: PHAsset?
func loadImageAsset(
targetSize: CGSize = PHImageManagerMaximumSize
) async {
if (asset != nil) {
guard let uiImage = try? await photoLibraryService
.fetchImage(
byLocalIdentifier: asset!.localIdentifier,
targetSize: targetSize
) else {
image = nil
return
}
image = Image(uiImage: uiImage)
} else {
image = nil
}
}
var body: some View {
ZStack {
// Show the image if it's available
if let image = image {
GeometryReader { proxy in
NavigationLink(destination: BigPicture(image: image)) {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(
width: proxy.size.width,
height: proxy.size.width
)
.clipped()
}
}
} else {
// Otherwise, show a gray rectangle with a
// spinning progress view
Rectangle()
.foregroundColor(.gray)
.aspectRatio(1, contentMode: .fit)
ProgressView()
}
}
// We need to use the task to work on a concurrent request to
// load the image from the photo library service, which
// is asynchronous work.
.task(id: asset?.localIdentifier) {
await loadImageAsset()
}
// Finally, when the view disappears, we need to free it
// up from the memory
.onDisappear {
image = nil
}
}
}