I'd like to update the wordings shown in the view by presenting the VNDocumentCameraViewController.
For example,
In this picture, I'd like to change,
Position the document in view -> Hello world
Cancel -> Stop
Auto -> Delete the button
I'm not sure what attributes VNDocumentCameraViewController has, so cannot override the values in the child class.
How is it possible to override these button title to a different value?
The short answer is that you are not allowed to do this if you re releasing to the App Store; Apple provides the UI and since they don't provide a way to customize it you are supposed to leave it as is. The longer answer is that you can walk the view hierarchy and figure out which labels contain which text and then you can simply change the text on these UILabels. Its bad to do this for a lot of reasons:
1) It can get you rejected from the app store
2) The implementation can change between version, breaking your app
3) The text is localized so your changes will not work in other languages
With that warning, here is code to do a DFS search to find a view that matches a certain predicate, for instance, being a UILabel will some specific text.
import SwiftUI
import PlaygroundSupport
let a = UIView()
let b = UIView()
let c = UIView()
let l = UILabel()
let z = UILabel()
a.addSubview(b)
a.addSubview(c)
b.addSubview(z)
c.addSubview(l)
l.text = "hello"
extension UIView {
func child(matching predicate: (UIView) throws -> Bool) rethrows -> UIView? {
for child in subviews {
if try predicate(child) {
return child
}
if let found = try child.child(matching: predicate) {
return found
}
}
return nil
}
}
print(l === a.child(matching: {($0 as? UILabel)?.text == "hello"}))
Related
I'm trying to add a similar UIContextMenu as you can find in iMessages. When you long press a message, the context menu with some options should display.
I use tableView(_:contextMenuConfigurationForRowAt:point:) and other methods. So far so good.
But I came to 2 problems which I'm not able to solve:
the biggest one is the change of a preview. When the context menu is displayed and you receive a new message (which causes tableview to reload), the preview will change its content. So suddenly there is a different message than you originally selected. But I don't get it why because tableview methods for context menu aren't called... how could I resolve that?
Apple Messages stops adding a new message. But for example Viber is still able to receive a new message while there is a context menu.
I wanted to handle it somehow like Apple with the help of tableView(_:willDisplayContextMenu:animator:) ... but there is a second problem - this method is only for iOS +14.0.. ! So there is no way how I can I know that there will be context menu prior iOS 14?
I'll appreciate any help. Thanks.
I was able to solve the first problem somehow. The main idea is using snapshots instead of cell's view. This way even if tableView gets reload, the snapshot stays same.
You have to implement these 2 methods and supply snapshot there:
tableView(_:previewForHighlightingContextMenuWithConfiguration:)
tableView(_:previewForDismissingContextMenuWithConfiguration:)
In my code it looks like this:
func tableView(_: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
guard
let messageId = configuration.identifier as? String,
let indexPath = dataSource.indexPath(for: messageId),
let cell = tableView.cellForRow(at: indexPath) as? YourCustomCell else {
return nil
}
return makeTargetedPreview(cell: cell)
}
func makeTargetedPreview(cell: YourCustomCell) -> UITargetedPreview? {
guard
let previewView = cell.viewYouWantToDisplay,
let snapshot = previewView.snapshotView(afterScreenUpdates: false)
else {
return nil
}
// 1. Prepare how should the view in the preview look like
let parameters = UIPreviewParameters()
parameters.backgroundColor = .clear
parameters.visiblePath = UIBezierPath(roundedRect: previewView.bounds, cornerRadius: previewView.layer.cornerRadius)
// 2. Prepare UIPreviewTarget so we can use snapshot
let previewTarget = UIPreviewTarget(
container: previewView,
center: CGPoint(x: previewView.bounds.midX, y: previewView.bounds.midY)
)
// 3. Return UITargetedPreview with snapshot
// important ! We can't use cell's previewView directly as it's getting changed when data reload happens
return UITargetedPreview(view: snapshot, parameters: parameters, target: previewTarget)
}
Note: implementation of previewForDismissing(...) is similar.
I want to know what was the name of the viewController the the button was tapped. I've already looked into the answer provided here. But I believe won't work if I have different containerViews on the screen...where the viewController for each button may be different. Hence I need a different solution.
So I wrote this function to recursively look until it finds a UIViewController.
extension UIView{
func getTypeOfMyViewController() -> UIViewController.Type?{
if let _super = superview as? UIViewController{ // logically wrong!
return type(of:_super)
}else if let _super = superview{
return _super.getTypeOfMyViewController()
}else{
assertionFailure("everything should end up being on some viewController")
return nil
}
}
}
The only problem is this line:
if let _super = superview as? UIViewController{
It gives me the following warning:
Cast from 'UIView?' to unrelated type 'UIViewController' always fails
superview is a UIView and I don't know how to extract the 'viewController' which contains the 'view'.
Question1: So How can I do that?
Additionally I would like to use the getTypeOfMyViewController function as such:
extension UIButton{
open override var accessibilityLabel: String?{
get {
return "\(getTypeOfMyViewController.self): \(titleLabel?.text ?? "Null")"
}
set{
// nothing particular
}
}
}
I'm doing this because I want to create a unique identifier for all button taps in my logging system.
Question2: Does Swift offer any easier solution to this?
A view controller is not a view, so it can never be a superview. You have the right idea, but you're looking at the wrong hierarchy. What you want is not the view hierarchy but the responder chain.
Walk up the responder chain until you come to the view controller:
var r : UIResponder = theButton
repeat { r = r.next! } while !(r is UIViewController)
let vc = r as! UIViewController
Is there a way to remove the TextView- and PreviewPart, marked in RED, from my ShareExtention?
Im using the default:
class ShareViewController: SLComposeServiceViewController
As ViewController
I want to just show the Destination Selector like below (I know how to create that)
I know that you can create your own ViewController but it would be very hard to rebuild the default one.
The Second thing is:
What method would you recommend to Transfer/Copy Data/Files and which to read the Main App's Document Directory?
I want to transfer/copy the selected Document to the DocumentsDirectory of my Main App. Both are in the same AppGroup. Bc if I just save the Url of the current Document in UserDefault, then I guess the Main App can't access it.
I need to read the Main App's Document Directory, because I need the file Hierachy so I can select the saving location.
Note: Just because there was some confusion in the comments for whatever reason: This is my own ShareExtention and not smo else's in UIActivityController
Not the best, but here's what I've got based on the docs:
override func viewDidLoad() {
super.viewDidLoad()
textView.isHidden = true
}
But then there's blank space. Perhaps it's better to put some placeholder text in and prevent user input. There are other options in the docs you can use for that, or you can mess with textView (which is a UITextView)...
Edit: Here's a complete example that shows the URL in the text field, grey and non-user-interactible.
class ShareViewController: SLComposeServiceViewController {
override func viewDidLoad() {
super.viewDidLoad()
textView.isUserInteractionEnabled = false
textView.textColor = UIColor(white: 0.5, alpha: 1)
textView.tintColor = UIColor.clear // TODO hack to disable cursor
getUrl { (url: URL?) in
if let url = url {
DispatchQueue.main.async {
// TODO this is also hacky
self.textView.text = "\(url)"
}
}
}
}
override func isContentValid() -> Bool {
// Do validation of contentText and/or NSExtensionContext attachments here
return true
}
func getUrl(callback: #escaping ((URL?) -> ())) {
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = item.attachments?.first as? NSItemProvider,
itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { (url, error) in
if let shareURL = url as? URL {
callback(shareURL)
}
}
}
callback(nil)
}
override func didSelectPost() {
getUrl { (url: URL?) in
if let url = url {
DispatchQueue.main.async {
print("url: \(url)")
}
}
}
}
override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return []
}
}
What worked for me is to use a custom solution instead of SLComposeServiceViewController. It looks the same, and can add any element I want (see image at the end and here's the code at the commit when it was implemented).
Steps:
(code) Change ShareViewController to simple UIViewController
(code) Add blur effect to ShareViewController
(storyboard) Add container view to ShareViewController
(storyboard) Add navigation controller
(storyboard) Embed navigation controller in ShareViewController's container view
Customize the view controllers in the navigation controller (see this SO thread for example)
Step 1. Change ShareViewController to simple UIViewController
import UIKit
class ShareViewController: UIViewController {
// ^^^^^^^^^^^^^^^^
Step 2. Add blur effect to ShareViewController
// ShareViewController continued from Step 1.
override func viewDidLoad() {
super.viewDidLoad()
// https://stackoverflow.com/questions/17041669/creating-a-blurring-overlay-view/25706250
// only apply the blur if the user hasn't disabled transparency effects
if UIAccessibilityIsReduceTransparencyEnabled() == false {
view.backgroundColor = .clear
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
//always fill the view
blurEffectView.frame = self.view.bounds
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.insertSubview(blurEffectView, at: 0)
} else {
view.backgroundColor = .black
}
// Do any additional setup after loading the view.
}
Step 3. Add container view to ShareViewController
Drag a Container View from the Object Library into the ShareViewController on the storyboard, and adjust dimension. For example:
Step 4. Add navigation controller
Drag a Navigation Controller from the Object Library to the storyboard.
Step 5. Embed navigation controller in ShareViewController's container view
Control-drag from the container view of ShareViewController to the navigation controller, select "Embed" from the menu. Should look similar to this:
Step 6. Customize the view controllers in the navigation controller (see this SO thread for example)
My result:
You can't.
When you use the share extension you will afterwards select an app to share to, thus plugging yourself to the share extension of said app.
Your app does not and cannot have any access of what said share extension does, those are totally separated from the main app runtime.
Also this allow users to customise what they share.
For the second part of your post you need to elaborate, who wants to transfer a document, you as the developper or your end-user?
I use a UISplitViewController with preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay and I was looking for a way to dismiss master view controller. My master contains a table view and I'd like to close it whenever I select a cell. Surprisingly UISplitViewController doesn't seem to offer a method for that (but I do see Apple Mail doing that we you select an email in portrait mode).
I found the following workaround reported here: Hiding the master view controller with UISplitViewController in iOS8 (look at phatmann answer). This works but it also creates a weird animation when it's dismissed, there's an underlying gray outlined view which is not animated together with my master's view. The problem has been reported also here: iOS Swift 2 UISplitViewController opens detail screen on master's place when on iPad/ iPhone 6+
The problem occurs only when I dismiss master with this workaround, not when I tap on the secondary so I guess UISplitViewController is not following the regular dismiss flow when you just call sendAction on the button.
I used the following code to address this problem. There might be a better way to match on the specific view that is causing the issue. That said, this code was in an app approved by Apple in April of this year. What the code does is look for a specific view of a certain type, and if found, then it makes it hidden until the animation is complete. Its somewhat future proof, since if it does't detect the special view, it does nothing. I also added some comments for adopters on where you might want to make changes.
func closePrimaryIfOpen(finalClosure fc: (() -> Void)? = nil) {
guard let
primaryNavController = viewControllers[0] as? MySpecialNavSubclass,
primaryVC = primaryNavController.topViewController as? MySpecialCalss
else { fatalError("NO Special Class?") }
// no "official" way to know if its open or not.
// The view could keep track of didAppear and willDisappear, but those are not reliable
let isOpen = primaryVC.view.frame.origin.x >= -10 // -10 because could be some slight offset when presented
if isOpen {
func findChromeViewInView(theView: UIView) -> UIView? {
var foundChrome = false
var view: UIView! = theView
var popView: UIView!
repeat {
// Mirror may bring in a lot of overhead, could use NSStringFromClass
// Also, don't match on the full class name! For sure Apple won't like that!
//print("View: ", Mirror(reflecting: view).subjectType, " frame: \(view.frame)")
if Mirror(reflecting: view).description.containsString("Popover") { // _UIPopoverView
for v in view.subviews {
//print("SV: ", Mirror(reflecting: v).subjectType, " frame: \(v.frame)")
if Mirror(reflecting: v).description.containsString("Chrome") {
foundChrome = true
popView = v
//popView.hidden = true
break
}
}
if foundChrome { break }
}
view = view.superview
} while view != nil
return popView
}
// Note: leave it as optional - Apple changes things and we don't find the view, things still work!
let chromeView = findChromeViewInView(self.view)
UIView.animateWithDuration(0.250, animations: {
chromeView?.hidden = true
self.preferredDisplayMode = .PrimaryHidden
}, completion: { Bool in
self.preferredDisplayMode = .PrimaryOverlay
chromeView?.hidden = false
if let finalClosure = fc {
finalClosure()
}
//print("SLIDER CLOSED DONE!!!")
} )
}
}
I'd like to use the UIViewController's input accessory view like this:
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView! {
return self.bar
}
but the issue is that I have a drawer like view and when I slide the view open, the input view stays on the window. How can I keep the input view on the center view like Slack does it.
Where my input view stays at the bottom, taking up the full screen (the red is the input view in the image below):
There are two ways to do this exactly like Slack doing it, Meiwin has a medium post here A Stickler for Details: Implementing Sticky Input Field in iOS to show how he managed to do this which he actually puts an empty UIView as an inputAccessoryView then track it’s coordinates on screen to know where to put his custom view in relation with the empty view, this way can be helpful if you are going to support SplitViewController on iPad, but if you are not interested in this way, you can see how I managed to do this like this image
Here is before swiping
Here is after
All I did was actually taking a snapshot from the inputAccessoryView window and putting it on the NavigationController of the TableViewController
I am using SideMenu from Jon Kent and it’s pretty easy to do it with the UISideMenuNavigationControllerDelegate
var isInputAccessoryViewEnabled = true {
didSet {
self.inputAccessoryView?.isHidden = !self.isInputAccessoryViewEnabled
if self.isInputAccessoryViewEnabled {self.becomeFirstResponder()}
}
}
func sideMenuWillAppear(menu: UISideMenuNavigationController, animated: Bool) {
let inputWindow = UIApplication.shared.windows.filter({$0.className == "UITextEffectsWindow"}).first
self.inputAccessoryViewSnapShot = inputWindow?.snapshotView(afterScreenUpdates: false)
if let snapShotView = self.inputAccessoryViewSnapShot, let navView = self.navigationController?.view {
navView.addSubview(snapShotView)
}
self.isInputAccessoryViewEnabled = false
}
func sideMenuDidDisappear(menu: UISideMenuNavigationController, animated: Bool) {
self.inputAccessoryViewSnapShot?.removeFromSuperview()
self.isInputAccessoryViewEnabled = true
}
I hope that helps :)