UISheetPresentationController displaying differently on different phones - ios

I created a UIStoryboardSegue to make a "Bottom Sheet segue". Our designer shared a screenshot of the app on his phone and the bottom sheet is displaying differently, despite the fact we are both on the same iOS version.
On mine and my simulator, when the bottom sheet opens, it lightens the source view and then shrinks it down a little, so it appears just barely behind the bottom sheet
On the same screen on the designers device, it dims the background and leaves the source view full size, showing the top of the buttons in the navigation bar
I've noticed the Apple maps bottom sheet behaves like the designers, no shrinking of the background view. But I can't see any settings that would affect this. How can I stop the sheet from resizing the source view on mine and function like it's supposed to?
Here's my code:
import UIKit
public class BottomSheetLargeSegue: UIStoryboardSegue {
override public func perform() {
guard let dest = destination.presentationController as? UISheetPresentationController else {
return
}
dest.detents = [.large()]
dest.prefersGrabberVisible = true
dest.preferredCornerRadius = 30
source.present(destination, animated: true)
}
}

Found a hack to force it to never minimise the source view at least, not really what I wanted, but keeps it consistent. Supposedly, .large() is always supposed to minimize the source view, you can avoid this in iOS 16 by creating a custom detent that is just a tiny bit smaller than large like so:
let customId = UISheetPresentationController.Detent.Identifier("large-minus-background-effect")
let customDetent = UISheetPresentationController.Detent.custom(identifier: customId) { context in
return context.maximumDetentValue - 0.1
}
dest.detents = [customDetent]
And as a bonus, found a way to control the dimming on the bottom sheet overlay. There is a containerView property on the presentationController, but it is nil when trying to access it in while in the segue. If you force code to run on the main thread, after the call to present, you can access the containerView and set your own color / color animation
e.g.
...
...
source.present(destination, animated: true)
DispatchQueue.main.async {
dest.containerView?.backgroundColor = .white
}

Related

Swift - Dark background of the ViewController that appears over the context

Here is the code that creates my custom popUp:
extension UIViewController {
public func presentPopup(animated: Bool, completion: (() -> Void)? = nil) {
let popup = MBPopUpViewController(accentColor: UIColor(hexString: "E40C15"),
popUpTitle: "Hello",
popUpMessage: "Test PopUp",
popUpFirstButtonLabel: "1",
popUpSecondButtonLabel: "2")
popup.modalPresentationStyle = .overCurrentContext
present(popup, animated: animated, completion: completion)
}
}
Is there a way, how to make the first view controller a bit darker with fade animation, when the second appears?
Here you can see what I have now.
You can see an example here, in photos app. When the AlertViewController appears, the background becomes darker. Are there any ideas how to achieve that? Thanks :)
This sounds like you want a Modal Popover.
It is possible to make the background color of the view being placed over another a transparent color. You will need to set the color and everything for that view in it's viewDidLoad.
Ensure that the segue is marked Cover Vertical that so there's no wipe animation, and it's a smooth fade. Also enable either Over Current Context or Over Full Screen depending on how you would like this to look.
The end result will look something like below depending on what elements you place in your second View Controller that you are segueing to. (NOTE: the animation displayed in the gif is sped up. And the dark grey area would contain whatever it is that you place in the view).
There's some great resources out there to help you with making these. I can recommend Mark Moeykens on YouTube, he makes great videos about things like this.

UITableView is resetting its background color before view appears

I'm using probably a little bit exotic way of initialization of my UI components. I create them programmatically and among them is a UITableView instance, I set its background color immediately upon initialization, like this:
class MyViewController: UIViewController {
...
let tableView = UITableView().tap {
$0.backgroundColor = .black
$0.separatorStyle = .none
}
...
}
where tap is extension function:
func tap(_ block: (Self) -> Void) -> Self {
block(self)
return self
}
This worked very well in my previous project which was created in Xcode 8 and then migrated to Xcode 9 without breaking anything. But now I've created brand new project in Xcode 9 and copy-pasted above-mentioned extension to it, but seems like something went wrong. When my view controller appears on screen table has white background and default separator insets.
This seems to affect only some of the properties because others are working as they should have (e.g. $0.register(nib: UINib?, forCellReuseIdentifier: String) registers required cell class and $0.showsVerticalScrollIndicator = false hides scroll indicator).
Perhaps some of you, guys, could give me an idea what's the heart of the matter.
Here's full code, to reproduce the issue simply create a new project and replace ViewController.swift's content. As you can see, table has correct rowHeight (160) but resets its background color.
As for "before view appears" statement: I've printed table's background color in viewDidLoad, viewWillAppear and viewDidAppear like this:
print(#function, table.backgroundColor.debugDescription)
– it changes its color only in the last debug print:
viewDidLoad() Optional(UIExtendedGrayColorSpace 0 1)
viewWillAppear Optional(UIExtendedGrayColorSpace 0 1)
viewDidAppear Optional(UIExtendedSRGBColorSpace 1 1 1 1)
I ended up moving the initialization to lazy var's function – turns out initializing UITableView during the initialization of it's view controller has some side effects.

UISearchBar briefly displays gray background when returning to title from a show segue

I have a customized UISearchController with a custom UISearchBar as the title of a navigation controller, so that there's a persistent search bar control visible throughout the changes of child view controllers, which is shown in the first image of my gallery (not enough reputation to post images yet):
http://imgur.com/a/IikEw
However, when a user taps a search result displayed in a child table view controller, the navigation title is replaced with a text string, so that it looks like the second image in my gallery.
The problem occurs when I navigate back to the table view showing the search results, because for a brief moment, the search bar has a light gray background that looks like the third image in my gallery.
And when I slow down the animations in the simulator, I'm able to enter the Debug View Hierarchy in Xcode to see what the offending element is, which turns out to be a UIImageView named "UISearchBarBackground" that exists only until the transition animation ends, and the color snaps back to the intended result, as shown in the fourth image in my gallery.
In the initial view controller that initializes and sets the properties of my UISearchController, I've set the following properties of the search bar in viewDidLoad:
let controller = CustomSearchController(searchResultsController: self.searchResultsController)
controller.searchBar.backgroundColor = UIColor.clear
controller.searchBar.backgroundImage = nil
...and I have these custom classes, which I implemented so that I could get rid of the search controller's cancel button, which won't stay removed with delegate statements:
class CustomSearchBar: UISearchBar {
override func setShowsCancelButton(_ showsCancelButton: Bool, animated: Bool) {
super.setShowsCancelButton(false, animated: false)
}
}
class CustomSearchController: UISearchController {
lazy var _searchBar: CustomSearchBar = {
[unowned self] in
let customSearchBar = CustomSearchBar(frame: CGRect.zero)
return customSearchBar
}()
override var searchBar: UISearchBar {
get {
return _searchBar
}
}
}
As far as I can understand, at no point is my custom search controller deinitialized, and it is only initialized once, so I feel like the problem is appearing because of the drawing cycle, which temporarily places a visible view with a gray background before my settings take effect.
I'm pretty much at a loss as to what exactly is going on here, and how to solve this issue. I'm wondering if I'm just overlooking something simple, or if I instead have to create a custom transition object to solve the problem. I'm about an intermediate level with iOS architecture and Swift, but am always looking to learn more.
I've figured out that this issue is caused by not having a value set to the optional barTintColor property of UISearchBar. If the property is nil during a transition animation, then the view will appear light gray until the animation ends. To solve the problem, I set controller.searchBar.barTintColor = UIColor(red: 76/255, green: 203/255, blue: 124/255, alpha: 1), and now I'm getting the intended results.

UICollectionView does not scroll after it has been initialized

I have a subclass of UICollectionViewController that is nested inside a UINavigationController. The collection contains several cells (currently, 3) and each cell is as big as the full screen.
When the whole thing is shown, the collection view initally scrolls to a specific cell (which works flawlessly for each cell):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // this is set in the beginning
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
}
}
However, the collection view refuses to scroll horizontally, hereafter, as if the user interaction was disabled. I am not sure what is happening, but this is what I have checked so far:
user interaction is enabled for the collection view
the next cell (right or left, depending on the scroll direction) is requested correctly which I found out by inspecting collectionView:cellForItemAtIndexPath:
the requested imagePath is the right one
scrollToItemAtIndexPath... does not work either if I try to trigger a scroll programmatically after everything has been loaded (nothing happens)
scrollRectToVisible... does neither
setting collectionView?.contentInset = UIEdgeInsetsZero before the programmatic scroll attempts take place does not change anything
the content size of the collection view is 3072x768 (= 3 screens, i.e. 3 cells)
Which bullet points are missing, here?
Although the post did not precisely tackle the root of my problem it forced me to ponder the code that I posted. If you look at it you will see that it basically says: Whenever the views need to be layouted, scroll to the cell at position currentlyPresentedPhotoCellIndexPath. However, and this you cannot see without any context, this variable is only set once, when the whole controller is being initialized. Thus, when you try to scroll, the layout changes, the controller then jumps back to the initial cell and it looks like nothing happens, at all.
To change this, you just have to enforce a single scroll, e.g. by doing this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // only once possible
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
currentlyPresentedPhotoCellIndexPath = nil // because of this line
// "initiallyPresentedPhotoCellIndexPath" would probably a better name
}
}
A big thanks to Mr.T!

Creating a floating menu in an iOS application

Looking to create a floating menu in Swift for an iOS application I am developing. Something along the lines of the little red circle menu as shown in the following image.
My initial thoughts were to extend the UIViewController class and add the respective drawing/logic there, however, the application is comprised of a few other controllers, more specifically the UITableViewController which in itself extends UIViewController. Is there perhaps a good place for an extension perhaps? Or is there a more eloquent way of drawing the menu on specific views without the mass duplication of menu related code?
The menu itself will be shown on most screens, so I need to selectively enable it. It'll also be somewhat contextual based on the view/screen the user is currently on.
Any awesome ideas?
You can create your own with the animations and all the things, or you can check this library
https://github.com/lourenco-marinho/ActionButton
var actionButton: ActionButton!
override func viewDidLoad() {
super.viewDidLoad()
let twitterImage = UIImage(named: "twitter_icon.png")!
let plusImage = UIImage(named: "googleplus_icon.png")!
let twitter = ActionButtonItem(title: "Twitter", image: twitterImage)
twitter.action = { item in println("Twitter...") }
let google = ActionButtonItem(title: "Google Plus", image: plusImage)
google.action = { item in println("Google Plus...") }
actionButton = ActionButton(attachedToView: self.view, items: [twitter, google])
actionButton.action = { button in button.toggleMenu() }
}
There is another alternative with this great library :
https://github.com/yoavlt/LiquidFloatingActionButton
You just have to implement the delegate and the dataSource in your ViewController:
let floatingActionButton = LiquidFloatingActionButton(frame: floatingFrame)
floatingActionButton.dataSource = self
floatingActionButton.delegate = self
You could use view controller containment. The menu can be its own view controller with its view laid transparently over top the content view controller.
For example this can be set up in the storyboard by dragging out two container views into a vanilla view controller.

Resources