Embedding Share Icons Inline Within Another View - ios

I am attempting to implement a "Share" feature in my iOS app with a similar look and feel to that of the Google Photos iOS app:
The bottom two rows of icons are what I care about. They look nearly identical to those displayed when using the UIActivityViewController.
In the Google Photos app, these icons appear inline with the "select photos" portion of the screen (e.g. you can still interact with the upper portion of the screen). However, the documentation for the UIActivityViewController states that "On iPhone and iPod touch, you must present [the view controller] modally."
This is the difference that is really important to me -- I'd like for the "share" icons to display inline with the rest of my content, rather than having a modal that is displayed on top of my content.
Is it possible to use the UIActivityViewController to achieve a similar effect shown in the screenshot above? If not, is there a recommended approach that I might use to implement this sort of functionality?

As discussed in another answer, reverse engineering UIActivityViewController is the only option in order to be able to achieve a similar effect. I tried this using iPhone 6s - 10.3 Simulator. The following findings may not be accurate for iOS 9.x or 11.x or above.
A. Find out all internal variables for UIActivityViewController
var variablesCount: UInt32 = 0
let variables = class_copyIvarList(UIActivityViewController.self, &variablesCount)
for i in 0..<variablesCount {
if let variable = variables?[Int(i)] {
let name = String(cString: ivar_getName(variable))
let typeEncoding = String(cString: ivar_getTypeEncoding(variable))
print("\(name)\n\(typeEncoding)\n\n")
}
}
free(variables)
The ones those got my attention at first sight are (in order) -
_activityViewController
#"UIViewController"
_contentController
#"_UIActivityViewControllerContentController"
_activityAlertController
#"UIAlertController"
On inspecting them further, I found out that _contentController is the one we should be looking for. We need to look one level deeper in hierarchy for UICollectionViewController to get to where we want to be.
if let activityContentController = activityVC.value(forKeyPath: "_contentController") as? UIViewController {
print("Found _contentController!")
for child in activityContentController.childViewControllers {
print(String(describing: child))
if child is UICollectionViewController {
print("Found UICollectionViewController!")
break
}
}
}
Why did I look for UICollectionViewController?
Debug View Hierarchy has the answer for this.
I tried adding this as a childViewController to my UIViewController -
self.addChildViewController(child)
child.didMove(toParentViewController: self)
self.view.addSubview(child.view)
child.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
child.view.topAnchor.constraint(equalTo: self.view.topAnchor),
child.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
child.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
child.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
IT SHOWS UP CORRECTLY ONLY IF YOU HAVE LOADED/PRESENTED UIActivityViewController FIRST.
I was able to achieve this using a silent present/dismiss call -
self.present(activityVC, animated: false, completion: {
self.dismiss(animated: false, completion: nil)
})
IS THIS APP STORE SAFE? - Most likely not.
As soon as you start stealing the view(s) or viewController(s) from UIKit's standard components, the behavior is not stable and it will break with upcoming updates for sure.
What Google Photos has is the result of way more advanced reverse engineering. In above implementation, you can't see More option screen. The hierarchy UIActivityViewController expects is broken.
Hope this helps.

Okay, I thought about this and I did some intensive research on the web but nobody ever seemed to needed to modify it like you want. So here are my guesses how Google engineers solved this:
They reverse engineered the UIActivityViewController and call some private APIs to get the same icons to show up and the reordering controllers
They use the UIViewController transitioning API and hack the view hierarchy of a modally presented UIActivityViewController, removing the cancel button and adding some custom views on top of its view
An indicator for the second option could be that the top of the presented "sheet" has a white background while the bottom has the greyish color.
Unfortunately I'm not very fit with the transitioning API as I'm just about to learn it but my understanding is that you can provide a custom object as the transitioning delegate.
This object then gets called when you present/dismiss or push/pop a UIViewController. It will get a reference to both, the presenting and the presented view controller and you can have fun with both of their views.
This should make it "quiet" easy to remove and add some subviews, change frames and colors etc while still having all the default behavior.
I hope this answer helps you to achieve what you want. However, be aware that the structure of the controller could change at any time so always make sure to also test agains betas so that you don't get caught by surprise when apple releases an update which breaks your UI. :)

Related

UISplitViewController - replace back-chevron icon with a sidebar toggle icon in portrait

I'm working on an iPadOS app in which I use UISplitViewController. So far I have been using old APIs and handling everything (including the displayModeButtonItem) manually. Now I wanted to migrate to the newer "column style" APIs (super.init(style: .doubleColumn). I had a few UI issues, but I managed to fix them with a few workarounds, but now I'm blocked on an issue for which I can't find any solution:
I'd like to make use of all built-in mechanisms of the iOS14's UISplitViewController, so I set presentsWithGesture property to true. Because of this I get sidebar toggle icon in landscape orientation (which works exactly how I want it), but in portrait I get a "back-chevron icon" with "back" title. Is there a way to force sidebar toggle icon to be displayed for both orientations?
I set up a sample project and I'm not getting the behavior you are. I'm thinking it's because we do things differently in setting up the UISplitViewController.
First, a major thanks to Matt Neuburg (#matt here on SO) for his two part article. (part one here) This provided me with some big changes to my code.
Basically, I no longer set my UISplitViewController to be the root view in my scenes. Instead, I do not touch SceneDelegate at all, but use the default ViewController:
let primaryVC = PrimaryVC()
let secondaryVC = SecondaryVC()
let compactVC = CompactVC()
override func viewDidLoad() {
super.viewDidLoad()
let split = UISplitViewController(style: .doubleColumn)
self.addChild(split)
self.view.addSubview(split.view)
split.view.frame = self.view.bounds
split.presentsWithGesture = true
split.preferredSplitBehavior = .tile
split.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
split.didMove(toParent: self)
split.setViewController(primaryVC, for: .primary)
split.setViewController(secondaryVC, for: .secondary)
split.setViewController(compactVC, for: .compact)
}
When I do this, I get a "sidebar.left" button and no "Back" button with Chevron. I'm using Xcode 13.1 and tried this with targets of iPadOS 14.0 and 15.1.
If you need to add/replace a bar button, what I do is:
var barBtnSidebarLeft = UIBarButtonItem(image: UIImage(systemName:"sidebar.left"), style: .plain, target: self, action: #selector(leftSidebarTapped))
navigationItem.leftBarButtonItem = barBtnSidebarLeft
But beware that if you do this, you will get two sidebar buttons! With regards to bar buttons, one more note - while the UISplitViewController provides a navigation bar by default for Primary and Secondary, it does not for Compact. (Frankly, I don't think it's good practice to have one when in compact size.)
Finally, read through the link I provided. It really gets into using UISplitViewControllerDelegate to help you pass along your scene state values properly. (I prefer calling it scene state instead of app state because iPads can have multiple app windows.)
EDIT
One last thought. IIRC you can do this:
set presentsWithGesture to `false
place your own bar buttons as pleased in both view controllers
manually (through the delegate functions?) open and close the Primary view controller

UIDocumentPickerViewController buttons look disabled but work -- why?

I am using UIDocumentPickerViewController to export images to the Files app in iOS 11. The picker comes up, and I am able to pick a destination for the files. I get the callback that the files were moved and I can see them in Files, so that appears to be working fine. However, the UI is wrong -- the Add and Cancel buttons look disabled, but they actually work fine. The buttons don't look this way if I just use Save to Files in the Share Sheet.
This is my code:
UIDocumentPickerViewController *docPicker =
[[UIDocumentPickerViewController alloc] initWithURLs:self.assetURLs inMode:UIDocumentPickerModeExportToService];
docPicker.delegate = self;
[vc presentViewController:docPicker animated:YES completion:nil];
Things I have tried:
1) I have tried both Move and Export types
2) I have enabled the iCloud capability (all three items: key-value, Document, and CloudKit) - no difference in the experience [in fact, the code worked even without these set, though the docs say otherwise])
Here is an screenshot of what the UI looks like. As you can see, the UI says it will add the item to the chosen directory (and it, in fact, does if I tap on Add):
Anyone know how to get the buttons to appear blue and look enabled? I have not found any sample code online that I could try -- if someone has a pointer, I can try that to see if it's something about my configuration or code.
I'm not a big fan of temporarily changing the global appearance like suggested in the approved answer. You can just reset the appearance for UIDocumentPickerViewController and the bar buttons will return to their original blue.
if #available(iOS 11.0, *) {
UINavigationBar.appearance(whenContainedInInstancesOf: [UIDocumentBrowserViewController.self]).tintColor = nil
}
Based on your comment, your app is setting the tintColor using UINavigationBar.appearance. This will affect all navigation bars, including those of system navigation controller such as UIDocumentPickerViewController.
I've dealt with this in one of my own apps. One solution is to subclass UIDocumentPickerViewController and to use your subclass anywhere you need the picker view. In your subclass, override viewDidLoad and set UINavigationBar.appearance.tintColor back to nil. And also override viewWillDisappear to reset UINavigationBar.appearance.tintColor back to the desired color.

How to reproduce Apple's alert-like views

I am struggling the last couple of days to figure out if these alerts are part of UIKit or just a private custom view. I don't even know how they properly called. The UI team have asked for an alert that looks like the one appears in Apple's News and Podcast apps. See screenshots below.
Apple's code to perform this is not part of UIKit. It will need to be implemented by you! I made a framework to do just this on GitHub called AOModalStatusView (https://github.com/alecdoconnor/AOModalStatusView).
The easiest way to do this would be with a custom view presented modally and with the presentation style set to "over current context" so that what is behind the view shows through. Inside the view create a square that is centered on the view. Give it rounded corners and a specific width and height. In order to get the blurred background you should use Apple's "Visual Effects View with Blur" or "Visual Effects View with Blur and Vibrancy." (https://shrikar.com/ios-development-tutorial-visual-blur-with-uivisualeffectview/)
In the view controller for this view, set a timer shortly in viewDidAppear(..) that will run dismiss(animated: true, completion: nil)
You can use my own StatusAlert framework written in Swift. It gives you ability to show Apple system-like alert as well as present the same alert without an image, title or message anywhere in UIView.
It is available via Cocoapods and Carthage and supports iPhone X, Safe Areas layout, iPads and allows some customizations.

How to fade in/out navigationBar on iOS 9?

Built-in Photo application fades in/out navigationBar when you tap on an image . This way Photo app allows to see it full screen.
How does it do this (fade efect)?
As I understand navigationController?.navigationBar.alpha doesn't work anymore (so you can't animate it this way).
Sharing all my finding.
Complain mode on
Frankly, I feel half pissed/like a dummy that I had to fight a good day to implement simple thing existing in Apple app.
Complain mode off
First of all here is some context. I am working with navigationBar which are provided by navigationController (vs just standalone bars which are manually dropped in your view)
There are several approaches which I found. I will mention all of them (even if I had no success using them)
1) Animate change of alpha of navigationBar
UIView.animateWithDuration(0.1, animations: {
navigationController?.navigationBar.alpha = 0
}, completion: nil)
#rmaddy mention here that it works for him. However, I believe he has a standalone bar (vs a bar managed by navigationController).
I used a tool Reveal to check UI hierarchy and found couple of things.
- There is a navigationBar which is hidden (and navigationController?.navigationBar is referencing it). So you can change alpha to your hearts joy, but these changes won't be visible.
There is however another navigationBar . I assume it's referenced in some private members of navigationController (let's call it private navigationBar). It's visible and that's what is displayed at the top of your view.
2) Use setNavigationBarHidden:animated:
This is a standard way to hide/show navigation bar. It's animated different way (it slides/up and down). However, if it's ok for you, just go with this is, because it's simple and clean.
navigationController?.setNavigationBarHidden(true, animated: true)
Additionally you can wrap it in UIView.beginAnimations, UIView.commitAnimations to animate it together with some other stuff (to make it smoother)
3) Animate change of alpha of private navigation bar.
This worked for me:
let privateNavigationBar = self.superview?.superview?.superview?.superview?.superview?.superview?.subviews[1]
UIView.animateWithDuration(0.1, animations: {
privateNavigationBar.alpha = 0
}, completion: nil)
I am going way up through the hierarchy to get a view which contains private navigationBar (which is second subview for that view).
However, this approach has multiple downsides:
I believe # of superviews? depends on your app hierarchy (vs you are using split view and so on). I think you can generalize or may be you just walk the whole hierarchy to find non hidden UINavigationBar to solve this.
I have a feeling that Apple may frown at this (your app be not accepted to AppStore)
4) Make navigationBar transparent and set background image to be transparent and change alpha channel on it.
I can't find where I read about this idea. There was couple of mentioning.
There is Apple example app which shows how to customize NavigationBar, including making it transparent.
It's interesting that this example app works for me (the navigation bar is transparent in it). However, when I tried this code in my app it didn't work (I still didn't figured out what is going on with this). As usual there are bunch of variables (may be something in Info.plist, also they subclass NavigationController, also may be something in view hierarchy)
5) Adding standalone navigationBar
You can hide a bar provided by navigationController. Add your own to the UIView, wire it to #IBOutlet and use alpha animation on it (most likely that's what #rmaddy was referring too).
I checked and this is work.
This approach is used in this tutorial.
However, it has a downside:
I believe it won't handle well rotation, increase of statusbar height while call or GPS
Each time when I see a code like this (written in the article) I know that there will be problems with resizing: CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 64.0)
You can potentially replace it with constrains. I went this route, but stumble upon some issues.
6) Other methods
I saw two more methods. I don't know whether they will work or what will be downsides:
One of them in this question: How to hide/show status bar and navigation bar by fading in/out at the same time like the Photos app in iOS 7?
And this answer: https://stackoverflow.com/a/18063898/422080

Have Slide menu cover view controller, not move it aside (iOS)

In my iOS app I am using SWRevealViewController to add menu functionality. It works great, however my client wants to make the menu cover the current view controller, not move it to the right.
I mean something like this:
before:
after:
The author of this library told me to implement the following, where I can get current offset of the menu and move the current view controller by that offset by doing something like that:
let rvrw = self.revealViewController().rearViewRevealWidth
var bounds = navigationController!.view.bounds
bounds.origin.x = rvrw
navigationController!.view.bounds = bounds
The problem is that the menu is behind it. I would like to know how I can do that in this library or if there is another library with similar implementation and that functionality as well.
I have found tons of libraries, but all of them are pushing the controller not covering it. I have found one or two which do it way I need, but they missed some features (like being under navigation bar), were outdated or buggy.
SWRevealViewController offers some customization options
documented in the SWRevealViewController.h header file.
It seems as though the frontView is the part with "WatchKit" in your example and the rearView is the menu.
In that case, you might be able to use setFrontViewPosition with the value FrontViewPositionLeftSide, which gives the frontView
// Left position, front view is presented left-offseted by rightViewRevealWidth
If that does not work, you could also try FrontViewPositionLeftSideMost, which gives the position
// Left most position, front view is presented left-offseted by rightViewRevealWidth+rigthViewRevealOverdraw
I did not have the setup at hand to try this.

Resources