-
Notifications
You must be signed in to change notification settings - Fork 399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Accept limited photo library authorization #22
base: main
Are you sure you want to change the base?
Changes from all commits
d46f969
7878b28
5cd6f12
3d4b017
74b7bf4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key> | ||
<true/> | ||
</dict> | ||
</plist> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,21 +6,20 @@ import Photos | |
import os.log | ||
|
||
class PhotoLibrary { | ||
|
||
static func checkAuthorization() async -> Bool { | ||
switch PHPhotoLibrary.authorizationStatus(for: .readWrite) { | ||
static func checkAuthorization(status: PHAuthorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)) async -> Bool { | ||
switch status { | ||
case .authorized: | ||
logger.debug("Photo library access authorized.") | ||
return true | ||
case .notDetermined: | ||
logger.debug("Photo library access not determined.") | ||
return await PHPhotoLibrary.requestAuthorization(for: .readWrite) == .authorized | ||
return await checkAuthorization(status: PHPhotoLibrary.requestAuthorization(for: .readWrite)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I think this fixes the issue! I was only considering limited authorization when checking if already authorized. I also needed to accept limited authorization at the time of request. Let me know if you want me to restructure this code somehow. I think the recursive nature is a bit weird, but I assume the OS won't return It's worth noting there's a minor logging behavior change here. Previously, you wouldn't log the new status after requesting authorization. Now, we do. If you want me to refactor this, let me know how! If logging new status after request isn't your preference, I can just do something simple like |
||
case .denied: | ||
logger.debug("Photo library access denied.") | ||
return false | ||
case .limited: | ||
logger.debug("Photo library access limited.") | ||
return false | ||
return true | ||
case .restricted: | ||
logger.debug("Photo library access restricted.") | ||
return false | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// | ||
// LimitedLibraryPicker.swift | ||
// Queryable | ||
// | ||
// Created by Jaden Geller on 10/30/23. | ||
// | ||
|
||
import SwiftUI | ||
import PhotosUI | ||
|
||
struct LimitedLibraryPicker: UIViewControllerRepresentable { | ||
@Binding var isPresented: Bool | ||
|
||
func makeUIViewController(context: Context) -> UIViewController { | ||
.init() | ||
} | ||
|
||
class Coordinator { | ||
var isPresented = false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coordinator tracks whether or not the picker is actually presented. Binding tracks whether or not the view wants it to be presented. This allows us to: (a) prevent attempting to present the picker again when it is already presented, and (b) update the binding value back to Technically, neither of these code paths are exercised by the app, but it seemed worth implementing this properly in case the calling code changes. |
||
} | ||
func makeCoordinator() -> Coordinator { | ||
.init() | ||
} | ||
|
||
func updateUIViewController(_ controller: UIViewController, context: Context) { | ||
if isPresented, !context.coordinator.isPresented { | ||
Task { | ||
context.coordinator.isPresented = true | ||
await PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: controller) | ||
context.coordinator.isPresented = false | ||
await MainActor.run { isPresented = false } | ||
} | ||
} | ||
else if !isPresented, context.coordinator.isPresented { | ||
Task { | ||
await MainActor.run { isPresented = true } | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension View { | ||
func limitedLibraryPicker(isPresented: Binding<Bool>) -> some View { | ||
overlay(LimitedLibraryPicker(isPresented: isPresented)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't actually matter if this is an overlay, background, etc. It just matters that the UIViewControllerRepresentable is part of the view hierarchy so it can get a reference to a UIViewController to present the limited library picker. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,7 +64,7 @@ struct SearchResultsView: View { | |
case .HAS_RESULT: | ||
// Has result | ||
VStack { | ||
if photoSearcher.totalUnIndexedPhotosNum > 0 { | ||
if photoSearcher.totalUnIndexedPhotosNum > 0 || PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Always show the button if the library has a limited authorization status, since it's always true that there may be more photos the user could index. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reasonable. |
||
UpdateIndexView(goToIndexView: $goToIndexView, photoSearcher: photoSearcher) | ||
} | ||
|
||
|
@@ -248,6 +248,7 @@ struct TipsView: View { | |
|
||
|
||
struct UpdateIndexView: View { | ||
@State var showLimitedLibraryPicker = false | ||
@Binding var goToIndexView: Bool | ||
@ObservedObject var photoSearcher: PhotoSearcher | ||
|
||
|
@@ -256,13 +257,23 @@ struct UpdateIndexView: View { | |
.accessibilityAddTraits(.isLink) | ||
.accessibilityHint(Text("Click to build index for new photos to make them searchable")) | ||
.foregroundColor(Color.weakgreen) | ||
.limitedLibraryPicker(isPresented: $showLimitedLibraryPicker) | ||
.onTapGesture { | ||
goToIndexView = true | ||
if PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited { | ||
showLimitedLibraryPicker = true | ||
} else { | ||
goToIndexView = true | ||
} | ||
} | ||
.onChange(of: showLimitedLibraryPicker) { showLimitedLibraryPicker in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit of a confusing state machine, but I don't know of a better way to represent this here. If the authorization is limited, instead of immediately going to index view, show the picker. Then, when the picker is dismissed, refetch the photos and go to the index view. |
||
if !showLimitedLibraryPicker { | ||
Task { await photoSearcher.fetchPhotos() } | ||
goToIndexView = true | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
||
struct SearchResultsView_Previews: PreviewProvider { | ||
static var previews: some View { | ||
SearchResultsView(goToIndexView: .constant(false), photoSearcher: PhotoSearcher()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whoops almost forgot to swap this out
I think I don't like using the default argument in this way, but waiting for feedback re: logs & style before changing anything