I am using PHPickerViewController to pick Image for User Profile Picture Purpose in iOS 15. I am using UIKit framework. I have the following code:
var pickerConfig = PHPickerConfiguration(photoLibrary: .shared())
pickerConfig.selectionLimit = 1
pickerConfig.filter = .images
let pickerView = PHPickerViewController(configuration: pickerConfig)
pickerView.delegate = self
self.present(pickerView, animated: true)
The Picker is working properly for selecting images and delegating the results. But, when the Cancel button is pressed, nothing happens and the Picker is not dismissed as expected.
How to dismiss the PHPickerViewController instance when its own Cancel button is pressed ?
Edit:
The implementation of PHPickerViewControllerDelegate Method is as follows:
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult])
{
results.first?.itemProvider.loadObject(ofClass: UIImage.self) { [unowned self] reading , error in
guard let image = reading as? UIImage, error == nil else
{
DispatchQueue.main.async {
picker.dismiss(animated: true)
self.profilePictureHasError = true
self.toggleDoneButtonEnabled()
}
return
}
self.profilePictureHasError = false
DispatchQueue.main.async {
picker.dismiss(animated: true)
self.profilePictureHasChanged = self.userProfilePicture != image
if self.profilePictureHasChanged
{
self.profilePictureView.image = image
self.toggleDoneButtonEnabled()
}
}
}
}
You need to dismiss the picker yourself in the picker(_:didFinishPicking:) delegate method which is called when the user completes a selection or when they tap the cancel button.
From the Apple docs for picker(_:didFinishPicking:):
The system doesn’t automatically dismiss the picker after calling this method.
For example:
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
// Do something with the results here
picker.dismiss(animated: true)
}
Your current delegate code only calls picker.dismiss when the results array is non-empty (i.e when the user has selected images). When the cancel button is tapped, the delegate method is called with an empty results array.
Fix the issue by adding the following to the top of the code in your delegate method:
if results.isEmpty {
picker.dismiss(animated: true)
return
}
you just wrap it out in an objc func for making cancel button works
#objc
func didOpenPhotos() {
lazy var pickerConfig = PHPickerConfiguration()
pickerConfig.filter = .images
pickerConfig.selectionLimit = 1
let pickerView = PHPickerViewController(configuration: pickerConfig)
pickerView.delegate = self
self.present(pickerView, animated: true)
}
call it anywhere
Related
I have a project using SwiftUI that requires CloudKit sharing, but I'm unable to get the UICloudSharingController to play nice in a SwiftUI environment.
First Problem
A straight-forward wrap of UICloudSharingController using UIViewControllerRepresentable yields an endless spinner (see this). As has been done for other system controllers like UIActivityViewController, I wrapped the UICloudSharingController in a containing UIViewController like this:
struct CloudSharingController: UIViewControllerRepresentable {
#EnvironmentObject var store: CloudStore
#Binding var isShowing: Bool
func makeUIViewController(context: Context) -> CloudControllerHost {
let host = CloudControllerHost()
host.rootRecord = store.noteRecord
host.container = store.container
return host
}
func updateUIViewController(_ host: CloudControllerHost, context: Context) {
if isShowing, host.isPresented == false {
host.share()
}
}
}
final class CloudControllerHost: UIViewController {
var rootRecord: CKRecord? = nil
var container: CKContainer = .default()
var isPresented = false
func share() {
let sharingController = shareController
isPresented = true
present(sharingController, animated: true, completion: nil)
}
lazy var shareController: UICloudSharingController = {
let controller = UICloudSharingController { [weak self] controller, completion in
guard let self = self else { return completion(nil, nil, CloudError.controllerInvalidated) }
guard let record = self.rootRecord else { return completion(nil, nil, CloudError.missingNoteRecord) }
let share = CKShare(rootRecord: record)
let operation = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: [])
operation.modifyRecordsCompletionBlock = { saved, _, error in
if let error = error {
return completion(nil, nil, error)
}
completion(share, self.container, nil)
}
self.container.privateCloudDatabase.add(operation)
}
controller.delegate = self
controller.popoverPresentationController?.sourceView = self.view
return controller
}()
}
This allows the controller to come up normally, but...
Second Problem
Tap the close button or swipe to dismiss and the controller will disappear, but there's no notification that it's been dismissed. The SwiftUI view's #State property that initiated presenting the controller is still true. There's no obvious method to detect dismissal of the modal. After some experimenting, I discovered the presenting controller is the original UIHostingController created in the SceneDelegate. With some hackery, you can inject an object that is referenced in a UIHostingController subclass into the CloudSharingController. This will let you detect the dismissal and set the #State property to false. However, all nav bar buttons no longer function after dismissing so you could only ever tap this thing once. The rest of the scene is completely functional, but buttons in the nav bar don't respond.
Third Problem
Even if you could get the UICloudSharingController to present and dismiss normally, tapping on any of the sharing methods (Messages, Mail, etc) makes the controller disappear with no animation and the controller for the sharing URL doesn't come up. No crash or console messages--it just disappears.
Demo
I made a quick and dirty project on GitHub to demonstrate the issue: CloudKitSharing. It just creates a single String and a CKRecord to represent it using CloudKit. The interface displays the String (a UUID) with a single nav bar button to share it:
The Plea
Is there any way to use UICloudSharingController in SwiftUI? Don't have the time to rebuild the project in UIKit or a custom sharing controller (I know--the price of being on the bleeding edge 💩)
I got this working -- initially, I wrapped the UICloudSharingController in a UIViewControllerRepresentable, much like the link you provided (I referenced that while building it), and simply adding it to a SwiftUI .sheet() view. This worked on the iPhone, but it failed on the iPad, because it requires you to set the popoverPresentationController?.sourceView, and I didn't have one, given that I triggered the sheet with a SwiftUI Button.
Going back to the drawing board, I rebuilt the button itself as a UIViewRepresentable, and was able to present the view using the rootViewController trick that SeungUn Ham suggested here. All works, on both iPhone and iPad - at least in the simulator.
My button:
struct UIKitCloudKitSharingButton: UIViewRepresentable {
typealias UIViewType = UIButton
#ObservedObject
var toShare: ObjectToShare
#State
var share: CKShare?
func makeUIView(context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) -> UIButton {
let button = UIButton()
button.setImage(UIImage(systemName: "person.crop.circle.badge.plus"), for: .normal)
button.addTarget(context.coordinator, action: #selector(context.coordinator.pressed(_:)), for: .touchUpInside)
context.coordinator.button = button
return button
}
func updateUIView(_ uiView: UIButton, context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UICloudSharingControllerDelegate {
var button: UIButton?
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
//Handle some errors here.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return parent.toShare.name
}
var parent: UIKitCloudKitSharingButton
init(_ parent: UIKitCloudKitSharingButton) {
self.parent = parent
}
#objc func pressed(_ sender: UIButton) {
//Pre-Create the CKShare record here, and assign to parent.share...
let sharingController = UICloudSharingController(share: share, container: myContainer)
sharingController.delegate = self
sharingController.availablePermissions = [.allowReadWrite]
if let button = self.button {
sharingController.popoverPresentationController?.sourceView = button
}
UIApplication.shared.windows.first?.rootViewController?.present(sharingController, animated: true)
}
}
}
Maybe just use rootViewController.
let window = UIApplication.shared.windows.filter { type(of: $0) == UIWindow.self }.first
window?.rootViewController?.present(sharingController, animated: true)
The problem here is that I'm presenting EditCommentVC modally, over the current context of the CommentVC because I want to set the background of the UIView to semi-transparent. Now, on the EditCommentVC I have a UITextView that allows the user to edit their comment, along with 2 buttons - cancel (dismisses the EditCommentVC) and update that updates the new comment and push it to the database.
In term of code, everything is working, except that once the new comment is being pushed and EditCommentVC is being dismissed, the UITableView on CommentsVC with all the comments is not being reloaded to show the updated comments. Tried calling it from viewWillAppear() but it doesn't work.
How can I reload the data in the UITableView in this case?
#IBAction func updateTapped(_ sender: UIButton) {
guard let id = commentId else { return }
Api.Comment.updateComment(forCommentId: id, updatedComment: editTextView.text!, onSuccess: {
DispatchQueue.main.async {
let commentVC = CommentVC()
commentVC.tableView.reloadData()
self.dismiss(animated: true, completion: nil)
}
}, onError: { error in
SVProgressHUD.showError(withStatus: error)
})
}
The code in the CommentVC where it transitions (and passes the id of the comment). CommentVC conforms to a CommentActionProtocol that passes the id of that comment:
extension CommentVC: CommentActionProtocol {
func presentActionSheet(for commentId: String) {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let editAction = UIAlertAction(title: "Edit", style: .default) { _ in
self.performSegue(withIdentifier: "CommentVCToEditComment", sender: commentId)
}
actionSheet.addAction(editAction)
present(actionSheet, animated: true, completion: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "CommentVCToEditComment" {
let editCommentVC = segue.destination as! EditCommentVC
let commentId = sender as! String
editCommentVC.commentId = commentId
}
}
}
I see atleast 2 problems here:
You are creating a new CommentVC which you should not do if you want to update the tableView in the existing view controller.
Since you have mentioned that Api.Comment.updateComment is a an asynchronous call, you need to write the UI code to run on the main thread.
So first you need to have the instance of the commentVC in a variable inside this viewController. You can store the instance of the view controller from where you are presenting this view controller.
class EditCommentVC {
var commentVCdelegate: CommentVC!
// Rest of your code
}
Now you need to pass the reference commentVC in this variable when you are presenting the edit view controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "CommentVCToEditComment" {
let editCommentVC = segue.destination as! EditCommentVC
let commentId = sender as! String
editCommentVC.commentId = commentId
editCommentVC.commentVCdelegate = self
}
}
Now you need to use this reference to reload your tableView.
Api.Comment.updateComment(forCommentId: id, updatedComment: editTextView.text!, onSuccess: {
DispatchQueue.main.async {
commentVCdelegate.tableView.reloadData() // - this commentVC must be an instance that you store of the your commentVC that you created the first time
self.dismiss(animated: true, completion: nil)
}
}, onError: { error in
SVProgressHUD.showError(withStatus: error)
})
Well, i had this problem too, and the solution i found was to use Protocol. I would recommend you to search how to send data back to previous ViewController. That way, when you dismiss the EditCommentVC, you then send back a value(in my case i send true) to the previous ViewController(in your case, CommentVC), and then you'll have a function in CommentVC checking if the value is true and if it is, reload the TableView.
Here, let me show you an example of how i used (those are the names of my ViewControllers, functions and protocols, you can use whatever you want and send whatever data you want back):
In your CommentVC, you'll have something like this:
protocol esconderBlurProtocol {
func isEsconder(value: Bool)
}
class PalestranteVC: UIViewController,esconderBlurProtocol {
func isEsconder(value: Bool) {
if(value){
//here is where you can call your api again if you want and reload the data
tableView.reloadData()
}
}
}
Also, dont forget that you have to set the delegate of EditCommentVC, so do it when you're presenting EditCommentVC, like this:
let viewController = (self.storyboard?.instantiateViewController(withIdentifier: "DetalhePalestranteVC")) as! DetalhePalestranteVC
viewController.modalPresentationStyle = .overFullScreen
viewController.delegate = self
self.present(viewController, animated: true, completion: nil)
//replace **DetalhePalestranteVC** with your **EditCommentVC**
And in your EditCommentVC you'll have something like this:
class DetalhePalestranteVC: UIViewController {
var delegate: esconderBlurProtocol?
override func viewWillDisappear(_ animated: Bool) {
delegate?.isEsconder(value: true)
}
}
That way, everything you dismiss EditCommentVC, you'll send back True and reload the tableView.
I started learning swift a few weeks ago and I am now moving on to something a little trickier: I would like to build a simple app which uses a UIPageViewController. Each page shall contain a video which is loaded from a server using a plugin called Player. Since I want individual videos to be player on the pages, I use an array which stores all the URLs. These URLs are then used to set a value in another class. If the value in this class was set (didSet), I load the video using the plugin. All that works perfectly fine! To get an idea of what I did you can click this link to this youtube tutorial which I used as an orientation.
However, I encountered a problem which I had already suspected: The videos are reloaded whenever one hits a page one has already been on and the video has already been loaded on once. Obviously, I don't want the videos to reload every time which is why I looked for a caching library for swift under awesome-swift. The first one suggested (HanekeSwift) somehow didn't work for me and gave me a bunch of errors every time I tried to include it. Thus, I tried to approach my problem using the second option provided: Carlos which didn't cause any errors (which is a library by a German news agency so it sounds and also looks very professional). Nevertheless, as I am quite new to swift, I cannot quite put my finger on this plugin. I don't understand a lot of things since I am quite new...
What would I have to add to my code to cache the videos? Do I need to create another class for that? How would you recommend me to do it? I'll provide what i have till now below... And sorry for the long (and newby) question but I really couldn't figure it out.
Code
PageViewController
//viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
//set datasourcee to self for now
dataSource = self
view.backgroundColor = .white
let postViewController = PostViewController()
postViewController.video = videos.first
let viewControllers = [postViewController]
//set first view controller
setViewControllers(viewControllers, direction: .forward, animated: true, completion: nil)
}
//videos array
let videos = ["https://link.to/testvideo.mov", "https://link.to/testvideo.mov", "https://link.to/testvideo.mov", "https://link.to/testvideo.mov"]
//after page
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let currentIndexString = (viewController as! PostViewController).index
let currentIndex = indec.index(of: currentIndexString!)
//next page possible?
if currentIndex! < indec.count - 1 {
let postViewController = PostViewController()
postViewController.video = videos[currentIndex! + 1]
return postViewController
}
return nil
}
//before page
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let currentIndexString = (viewController as! PostViewController).index
let currentIndex = indec.index(of: currentIndexString!)
//previous page possible?
if currentIndex! > 0 {
//template
let postViewController = PostViewController()
postViewController.video = videos[currentIndex! - 1]
return postViewController
}
return nil
}
PostViewController
var video:String? {
didSet {
print(video ?? String())
setupPlayer()
}
}
//orienting myself by the example provided by "Player" plugin from here on (https://github.com/piemonte/Player/blob/master/Project/Player/ViewController.swift)
fileprivate var player = Player()
deinit {
self.player.willMove(toParentViewController: self)
self.player.view.removeFromSuperview()
self.player.removeFromParentViewController()
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
}
func setupPlayer() {
self.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.player.playerDelegate = self as? PlayerDelegate
self.player.playbackDelegate = self as? PlayerPlaybackDelegate
self.player.view.frame = self.view.bounds
self.addChildViewController(self.player)
self.view.addSubview(self.player.view)
self.player.didMove(toParentViewController: self)
self.player.url = URL(string: video!)
self.player.playbackLoops = true
}
This question already has answers here:
How to do some stuff in viewDidAppear only once?
(9 answers)
Closed 6 years ago.
I have a set up for a picture selection as follows:
#IBOutlet weak var imageHolder: UIImageView!
#IBAction func addImage(_ sender: UIButton) {
let pickerController = UIImagePickerController()
pickerController.delegate = self
pickerController.sourceType = UIImagePickerControllerSourceType.photoLibrary
pickerController.allowsEditing = true
self.present(pickerController, animated: true, completion: nil)
}
var imageSaved = UIImage()
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) {
self.dismiss(animated: true, completion: nil)
self.imageHolder.image = image
imageSaved = image
}
Selecting the picker brings up all of the pictures on the simulator, then the user can select them. Once it is selected the view returns to the original view and displays the picture in the imageView. It is not saved, though, until a "Save" button is hit in this view. This all works fine. The save button is merely an action that saves the image file to the DB.
I have the viewDidAppear set up to query the DB and display the image that has been saved as soon as the user moves into that view... this also works well. However, since the viewDidAppear is run multiple times, there becomes an issue when a new image is first selected to replace the old one. The "Save" button deletes the old object and replaces it, so once it has been replaced everything works fine. However, after selecting the image in the picker (and before hitting "Save"... and therefore before that file is in the DB) the view briefly displays the newly selected the image, but the viewDidAppear runs again and sends it back to the image currently in the DB. Its not technically an error, but it is ugly and I would like it to be fixed but I am not sure how.
This is the viewDidAppear code also:
override func viewDidAppear(_ animated: Bool) {
let query = PFQuery(className: "ProfilePictures")
query.whereKey("user", equalTo: (PFUser.current()?.objectId!)! as String)
query.findObjectsInBackground { (objects, error) in
if let objectss = objects{
if objectss.count != 0{
for object in objectss{
let imagePulled = object["ProfilePicture"] as! PFFile
imagePulled.getDataInBackground(block: { (data, error) in
if let downloadedImage = UIImage(data: data!){
self.imageHolder.image = downloadedImage
} else{
print(error)
}
})
}
}
}
}
}
Essentially, how do I get the image to only show the newly selected image in that space between selecting it and hitting save?
You should move your initial query in viewDidLoad(). That only gets called once, so it won't replace your image each time the view appears.
Otherwise, if, for some reason, you need to have it in view did viewDidAppear(), you could use dispatch_once to make sure your query only runs the first time the view appears.
Rough example of using dispatch_once:
var token: dispatch_once_t = 0
dispatch_once(&token) { () -> Void in
// your query goes here
}
If you are using Swift 3, please see this other response on how to get dispatch_once like functionality.
I have a customVC nib view in which tableview items are displaying from url .Now on clicking a button here I need to reload the same viewcontroller with some other request sent to api . My data is again reloading and the response is coming but only tableview data is not changing based on the response. The old data is still loading...
class CustomAddOnVC: UIViewController,UITableViewDataSource,UITableViewDelegate {
var myAddOnUrl = "http://\(platform).eposapi.co.uk/?app_id=\(apiID)&app_key=\(apikey)&request=addon&aid=\(PASSADDON!)"
#IBAction func ContinueBtn(sender: AnyObject){
if checkCount == 0{
print("Please Select one Item")
}else{
if addOnNext != 0
{
//here i am changing my request for url
PASSADDON = String(addOnNext!)
print(PASSADDON)
let customVC = CustomAddOnVC()
customVC.modalPresentationStyle = .OverCurrentContext
presentViewController(customVC, animated: true, completion: nil)
dismissViewControllerAnimated(false, completion: nil)
}
}
my screen shots