Initial Commit
This commit is contained in:
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Frame 1(2).png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Swipe That Pic/Assets.xcassets/AppIcon.appiconset/Frame 1(2).png
Normal file
BIN
Swipe That Pic/Assets.xcassets/AppIcon.appiconset/Frame 1(2).png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
6
Swipe That Pic/Assets.xcassets/Contents.json
Normal file
6
Swipe That Pic/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
16
Swipe That Pic/Models/Extension.swift
Normal file
16
Swipe That Pic/Models/Extension.swift
Normal 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]
|
||||
}
|
||||
}
|
17
Swipe That Pic/Models/Item.swift
Normal file
17
Swipe That Pic/Models/Item.swift
Normal 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
|
||||
}
|
||||
}
|
98
Swipe That Pic/PhotosService.swift
Normal file
98
Swipe That Pic/PhotosService.swift
Normal 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
34
Swipe That Pic/Swipe_That_PicApp.swift
Normal file
34
Swipe That Pic/Swipe_That_PicApp.swift
Normal 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)
|
||||
}
|
||||
}
|
79
Swipe That Pic/Views/BigPicture.swift
Normal file
79
Swipe That Pic/Views/BigPicture.swift
Normal 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
|
||||
}
|
||||
}
|
70
Swipe That Pic/Views/ContentView.swift
Normal file
70
Swipe That Pic/Views/ContentView.swift
Normal 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)
|
||||
}
|
||||
|
53
Swipe That Pic/Views/IgnoredPictures.swift
Normal file
53
Swipe That Pic/Views/IgnoredPictures.swift
Normal 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)
|
||||
}
|
||||
|
73
Swipe That Pic/Views/PhotoTumbnailFromStringView.swift
Normal file
73
Swipe That Pic/Views/PhotoTumbnailFromStringView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
73
Swipe That Pic/Views/PhotoTumbnailView.swift
Normal file
73
Swipe That Pic/Views/PhotoTumbnailView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user