Rounded corners on iOS 13 page sheet - ios

Is there a way to round the corners on an iOS page sheet view controller? Currently, iOS page sheets by default present like this:
But instead, I would like the corners to be like this:

iOS 15 added an API to customize the corner radius of sheets, UISheetPresentationController.preferredCornerRadius:
let myViewController = UIViewController()
myViewController.view.backgroundColor = .systemYellow
myViewController.sheetPresentationController?.preferredCornerRadius = 25
present(myViewController, animated: true, completion: nil)

In your view controller, you can change the view.layer.cornerRadius property to the value you want
override func viewDidLoad() {
super.viewDidLoad()
view.layer.cornerRadius = 10.0 // You can freely change this value
}
As an example, the following code:
override func viewDidLoad() {
super.viewDidLoad()
view.layer.cornerRadius = 25.0
view.backgroundColor = .systemPurple
}
gives me the following result:

I found a way to make it work.
In the onAppear method of your sheet, get the viewcontroller that is displaying the sheet using UIApplication.shared.activeWindows.last?.rootViewController then get the sheet viewcontroller with presentedViewController and do your things on it.
struct Example: View {
#State var showSheet = true
var body: some View {
Text("Hello, World!")
.sheet(isPresented: $showSheet, content: {
Text("hello")
.onAppear {
if let controller = UIApplication.shared.activeWindows.last?.rootViewController {
if let presentedVC = controller.presentedViewController {
presentedVC.view.backgroundColor = .red
presentedVC.view.layer.cornerRadius = 30
}
}
}
})
}
}

Related

Update UIKit View From Hosted SwiftUI Selection

Have a UIKit Navigation Controller that is the root view controller in the hierarchy of the storyboards. One of the button items navigates to a UIHostedViewController that produces a SwiftUI View. That SwiftUI View is in and of itself a Tab View with tabs - and all of it works fine.
One of the crux of our development is displaying large data models for the user, and we could regain a bit of screen real estate if we utilize the Navigation bar of the UIKit Navigation.
A defining attribute of our individual views is the color coding of the header and footer, we have been able to change these colors easily as the SwiftUI Tab View just calls different views with different modifiers.
What we would like to do is have the selection of the SwiftUI Tab View set a shared property with the UIKit view that would then change the navigation bar color - however the way we setup a bound variable and reload the NavigationBar setup with a didSet on that variable does not seem to be working.
The code is almost exactly as follows, less a couple of views.
class SomeViewController: UIHostingController< MySwiftUITabView > {
var headerColor: Binding<UIColor> = .constant(UIColor.blue) {
didSet {
DispatchQueue.main.async(qos: .userInteractive) {
self.setupUI()
}
}
}
var defaultColor: UIColor!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder, rootView: MySwiftUITabView(headerColor: headerColor))
}
override func viewDidLoad() {
super.viewDidLoad()
defaultColor = self.navigationController?.navigationBar.backgroundColor
setupUI()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
teardownUI()
}
func setupUI() {
if let naviController = self.navigationController {
let newNavBarAppearance = UINavigationBarAppearance()
newNavBarAppearance.backgroundColor = headerColor.wrappedValue
naviController.navigationBar.scrollEdgeAppearance = newNavBarAppearance
naviController.navigationBar.compactAppearance = newNavBarAppearance
naviController.navigationBar.standardAppearance = newNavBarAppearance
}
}
func teardownUI() {
if let naviController = self.navigationController {
let oldNavBarAppearance = UINavigationBarAppearance()
oldNavBarAppearance.backgroundColor = defaultColor
naviController.navigationBar.scrollEdgeAppearance = oldNavBarAppearance
naviController.navigationBar.compactAppearance = oldNavBarAppearance
naviController.navigationBar.standardAppearance = oldNavBarAppearance
}
}
}
And the SwiftUI Tab View:
struct MySwiftUITabView: View {
#Binding var headerColor: UIColor
var body: some View {
TabView(selection: $selectedTab){
TestView()
.tag(0)
.onAppear {
headerColor = .orange
}
.tabItem {
Label("Test", image: "circle.fill")
}
TestView()
.tag(1)
.onAppear {
headerColor = .red
}
.tabItem {
Label("Test", image: "square.fill")
}
}
.accentColor(.white)
}
}
Would a callback be more appropriate for this? We wrapped the didSet function call in a DispatchGroup as a final attempt since it is updating the UI, but the was not effective still for what we are looking for.
In this example, the navigation bar remains the same color as the original set value (.blue) and never gets updated to the color set to the binding by the .onAppear of the swiftUI tab items.
Any help would be greatly appreciated as always!

iPadOS - SwiftUI in a Safari Extension Not Rendering Properly

I'm writing a simple credential autofill extension as a means of playing around with SwiftUI on iOS. However, I'm finding that on iPadOS the SwiftUI View does not render properly. My CredentialProviderViewController is called at the beginning of the extension lifecycle, and is responsible for loading the SwiftUI View. It looks like this:
class CredentialProviderViewController: ASCredentialProviderViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
let services: [String] = serviceIdentifiers.map { $0.identifier }
let autofillView = AutofillView(services: services,
extensionContext: self.extensionContext)
let vc = UIHostingController(rootView: autofillView)
vc.view.translatesAutoresizingMaskIntoConstraints = false
vc.view.frame = view.bounds
view.addSubview(vc.view)
addChild(vc)
}
}
My SwiftUI AutofillView is very simple and looks like this:
struct AutofillView: View {
let services: [String]
var extensionContext: ASCredentialProviderExtensionContext? = nil
var body: some View {
NavigationView {
Text("LOCK SCREEN")
}
}
}
On iPhone, this renders exactly as I'd expect, with the words "LOCK SCREEN" appearing in the center of the View when the extension loads. However, on iPad the View is displayed in a modal window and the contents are not rendered properly. In fact, only the slightest bit of the "L" is displayed. (See screenshot)
I'm sure I'm missing something or not instantiating my SwiftUI View properly. I'm just not sure where. Any thoughts?
Instead of adding it into a containerview or as a subview, present the UIHostingController like you would a view controller
let services: [String] = serviceIdentifiers.map { $0.identifier }
let autofillView = AutofillView(services: services,
extensionContext: self.extensionContext)
let vc = UIHostingController(rootView: autofillView)
vc.modalPresentationStyle = .fullScreen
self.present(vc, animated: false)

Swift UI: UIHostingController.view is not fit to content view size at iOS 13

I want to update Swift UI View according to the communication result.
But UIHostingController.view is not fit rootView size at iOS 13.
The same thing happens when I try with the sample code below.
I want to add self-sizing SwiftUI View to UIStackView, but SwiftUI View overlaps with the previous and next views is occurring because this problem.
How can I avoid this problem?
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let object = SampleObject()
let sampleView = SampleView(object: object)
let hosting = UIHostingController(rootView: sampleView)
hosting.view.backgroundColor = UIColor.green
addChild(hosting)
view.addSubview(hosting.view)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
hosting.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
hosting.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
hosting.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
hosting.didMove(toParent: self)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
object.test()
}
}
}
struct SampleView: View {
#ObservedObject var object: SampleObject
var body: some View {
VStack {
Text("test1").background(Color.blue)
Text("test2").background(Color.red)
if object.state.isVisibleText {
Text("test2").background(Color.gray)
}
}
.padding(32)
.background(Color.yellow)
}
}
final class SampleObject: ObservableObject {
struct ViewState {
var isVisibleText: Bool = false
}
#Published private(set) var state = ViewState()
func test() {
state.isVisibleText = true
}
}
If addSubview to UIStackView as below, the height of Swift UI View will not change in iOS13.
iOS13 (incorrect)
iOS14 (correct)
You have not set the bottom anchor, add this line
hosting.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
Another easy way is to set frame to hosting controller view and remove the constraint.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let object = SampleObject()
let sampleView = SampleView(object: object)
let hosting = UIHostingController(rootView: sampleView)
hosting.view.frame = UIScreen.main.bounds //<---here
hosting.view.backgroundColor = UIColor.green
addChild(hosting)
view.addSubview(hosting.view)
hosting.didMove(toParent: self)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
object.test()
}
}
}
Only for iOS 13
Try this:
Every time the view size change, call this:
sampleView.view.removeFromSuperview()
let sampleView = SampleView(object: object)
let hosting = UIHostingController(rootView: sampleView)
view.addArrangedSubview(hosting.view)

Adding a UIView/UIGestureRecognizer to the presented view in a UIPresentationController

I am trying to recreate the bottom drawer functionality seen in Maps or Siri Shortcuts by using a UIPresentationController by having it recognise user input and updating the frameOfPresentedViewInContainerView accordingly. However I want this mechanism to work independently of the presented UIViewController as much as possible so I'm trying to have the presentation controller add a handle area above the view. Ideally the view of the presented controller and the handle are should both recognise user input.
This works for the presented view, however any view I add to it responds to no UIGestureRecognizer at all. Am I missing something?
class PresentationController: UIPresentationController {
private let handleArea: UIView = UIView()
override var frameOfPresentedViewInContainerView: CGRect {
// Return some frame for now
return CGRect(x: 0, y: 250, width: containerView!.frame.width, height: 500)
}
override func presentationTransitionWillBegin() {
// Unwrap presented view
guard let presentedView = self.presentedView else {
return
}
// Set color
self.handleArea.backgroundColor = UIColor.green
// Add to view hierachy
presentedView.addSubview(self.handleArea)
// Set constraints
self.handleArea.trailingAnchor.constraint(equalTo: presentedView.trailingAnchor).isActive = true
self.handleArea.leadingAnchor.constraint(equalTo: presentedView.leadingAnchor).isActive = true
self.handleArea.bottomAnchor.constraint(equalTo: presentedView.topAnchor).isActive = true
self.handleArea.heightAnchor.constraint(equalToConstant: 56).isActive = true
self.handleArea.translatesAutoresizingMaskIntoConstraints = false
// These don't help
self.handleArea.isUserInteractionEnabled = true
presentedView.isUserInteractionEnabled = true
presentedView.bringSubviewToFront(self.handleArea)
}
override func presentationTransitionDidEnd(_ completed: Bool) {
if completed {
// Add gesture recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onHandleAreaTapped(sender:)))
self.handleArea.addGestureRecognizer(tapGestureRecognizer)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
// Remove subview
self.handleArea.removeFromSuperview()
}
// MARK: - Responder
#objc private func onHandleAreaTapped(sender: UITapGestureRecognizer) {
print("tap") // No output
}
}
I managed to solve it by adding both the handle area and the view of the presentedViewController to a custom view and then overriding the presentedView property and returning my custom view.

Change UIPopoverView background + arrow color

Is there a way to simply change the UIPopoverView background color (including its arrow) on iOS8?
(I did read a couple of articles on customizing "UIPopoverControllers". Does this apply here too, meaning the answer is "no"?)
Isn't this something I should be able to address in the prepareForSegue method triggering the popover? How can I reach the according view to change its appearance?
I found the solution. Subclassing is not necessary anymore with iOS8! The background can be accessed and changed like this from within the tableview -> navigation -> popoverPresentationController
self.navigationController?.popoverPresentationController?.backgroundColor = UIColor.redColor()
More information about this in WWDC session 2014.
You can simply modify popover like this:
let popoverViewController = self.storyboard?.instantiateViewControllerWithIdentifier("popoverSegue")
popoverViewController!.popoverPresentationController?.delegate = self
popoverViewController!.modalPresentationStyle = .Popover
let popoverSize = CGSize(width: 150, height: 60)
popoverViewController!.preferredContentSize = popoverSize
let popover = popoverViewController!.popoverPresentationController
popover?.delegate = self
popover?.permittedArrowDirections = .Up
popover?.sourceView = self.view
//change background color with arrow too!
popover?.backgroundColor = UIColor.whiteColor()
popover?.sourceRect = CGRect(x: self.view.frame.width, y: -10, width: 0, height: 0)
presentViewController(popoverViewController!, animated: true, completion: nil)
Seems like that popoverPresentationController.backgroundColor no longer works in iOS13.
Popover arrows now appear to take on the color of the popover viewController's view.backgroundColor.
Here's the whole code for the demo below:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let sourceButton = sender as? UIButton, let popover = segue.destination.popoverPresentationController {
popover.sourceView = sourceButton.superview
popover.sourceRect = sourceButton.frame
popover.permittedArrowDirections = [.left]
popover.delegate = self
segue.destination.preferredContentSize = CGSize(width: 100, height: 100)
//popover.backgroundColor = sourceButton.tintColor //old way
segue.destination.view.backgroundColor = sourceButton.tintColor //new way
}
}
#IBAction func btnTap(_ sender: Any) {
performSegue(withIdentifier: "popoverSegue", sender: sender)
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
SwiftUI : Xcode 11.5
Add the .background modifier with the color and add .edgesIgnoringSafeArea modifier.
.popover(isPresented: self.$vm.presentMenu, content: {
self.menuView
.background(Color.bgGray.edgesIgnoringSafeArea(.all))
})
Just adding that if you are using SwiftUI inside of a UIPopover or if you are using SwiftUI's popover modifier you can set the background color of the popover by just using a Color in the background, like as in a ZStack.
If you want the arrow colored you can add the .edgesIgnoringSafeArea(.all) modifier to the color in the background so it will extend into the arrow.
SwiftUI example:
import SwiftUI
struct PopoverTest: View {
#State var showing: Bool = true
var body: some View {
Button("Show") {
self.showing.toggle()
}
.popover(isPresented: $showing) {
ZStack {
Color.green.edgesIgnoringSafeArea(.all) // will color background and arrow
Text("Popover!")
}
}
}
}
struct PopoverTest_Previews: PreviewProvider {
static var previews: some View {
PopoverTest()
}
}

Resources