iPad: White text on one side of the status bar, black text on the other - uikit

The Home app on iPadOS 14 displays black text on the left side of the status bar, and white text on the right side. How is this achieved? Can it be done via public APIs?

Can it be done via public APIs?
Yes, it's all defined in UIBarStyle. Since each uiviewcontroller added to a splitview is embedded into its own UINavigationController, you can set the barstyle for each uiviewscontroller.navigationController!.navigationBar.barStyle
For white text, choose .black, and .default is black text.
myHomeViewController.navigationController!.navigationBar.barStyle = .black
you could also create your own viewcontrollers and define they're preferredstatusbarstyle to be .lightContent or .darkContent by overriding the childForStatusBarStyle as per the docs.
You can override the preferred status bar style for a view controller
by implementing the childForStatusBarStyle method.
As per Apples wwdc20 Build for iPad. Home was rebuilt using the new features of UISplitViewController.
Minus the button and fancy background. Home split view is a UISplitViewController initialized with the doubleColumn style. UISplitViewController(style: .doubleColumn)
Example of creating a split view in code only in SceneDelegate.swift of a new project:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
//UISplitViewControllerDelegate
splitViewController.delegate = self
//set which view is what
splitViewController.setViewController(sidebarViewController, for: .primary)
splitViewController.setViewController(myHomeViewController, for: .secondary)
//setup of sidebar and detail controllers
sidebarViewController.navigationController?.navigationBar.prefersLargeTitles = true
myHomeViewController.navigationController?.navigationBar.prefersLargeTitles = true
sidebarViewController.title = "Home"
myHomeViewController.title = "Home"
//appear side by side when a column is shown, use tile style.
splitViewController.preferredSplitBehavior = .tile
//'color' myHomeViewController statusbar to white, by setting barStyle = .black
myHomeViewController.navigationController!.navigationBar.barStyle = .black
//start styling your navigation controllers, As i've set preferesLargeTitles, style the navBars largeTitleTextAttributes
myHomeViewController.navigationController!.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
//style viewcontroller
myHomeViewController.view.backgroundColor = .blue
// always have primary and secondary sidebyside
//splitViewController.preferredDisplayMode = .oneBesideSecondary
splitViewController.show(.primary)
splitViewController.show(.secondary)
window = UIWindow(windowScene: windowScene)
window?.rootViewController = splitViewController
window?.makeKeyAndVisible()
}
Other helpful resources I found:
TLDW wwdc20 notes
UISplitViewController apple docs

This can be done only if the root view controller is a UISplitViewController. I've set one up with the new splitview column styles in iOS 14, but the older way should work too.
Then you need to get the navigationController from each viewController and set the barStyle on its navigationBar to .default or .black
After placing your splitViewController as the root viewController of your UIWindow in your scene function, set the style as below:
var embeddedSplitViewController: UISplitViewController?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
self.makeSplitViewController()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = self.embeddedSplitViewController
self.window = window
window.makeKeyAndVisible()
self.embeddedSplitViewController?.viewController(for: .primary)?.navigationController?.navigationBar.barStyle = .black
self.embeddedSplitViewController?.viewController(for: .secondary)?.navigationController?.navigationBar.barStyle = .default
}
}
func makeSplitViewController() {
let splitViewController = UISplitViewController(style: .doubleColumn)
// I have SwiftUI Views here but they could be any UIViewController
let primaryViewController = UIHostingController(rootView: SidebarView())
splitViewController.setViewController(primaryViewController, for: .primary)
splitViewController.setViewController(UIHostingController(rootView: DetailView()), for: .secondary)
let compactViewController = UIHostingController(rootView: SidebarView())
splitViewController.setViewController(compactViewController, for: .compact)
splitViewController.preferredPrimaryColumnWidth = 320
self.embeddedSplitViewController = splitViewController
}
This worked for me, and the statusBar reflected the barStyle and updated accordingly when showing and hiding the primary viewController.

Related

TabBar is not aligned properly in UISplitViewController

I dragged a SplitViewController onto the main storyboard and changed the root view to use a custom class: SplitViewController
Inside SplitViewController:
class SplitViewController: UIViewController {
let tbc = UITabBarController()
override func viewDidLoad() {
[..]
// I thought this would work
tbc.tabBar.itemPositioning = .centered
let vc1 = UIViewController()
vc1.title = "Home"
let vc2 = UIViewController()
let vc3 = UIViewController()
tbc.setViewControllers([vc1, vc2, vc3], animated: false)
// I was experimenting here
// self.splitViewController!.maximumPrimaryColumnWidth = 50000
// One view controller is hidden for demo purposes:
self.splitViewController!.viewControllers = [
UINavigationController(rootViewController: tbc),
UINavigationController(rootViewController: [..])
]
}
}
I got this on iPad:
Should I drag from far left to right, the sidebar extends a bit and I can see the "hidden" tabBarItem (Home). How to get all tabBarItems to be in view?

Saving image from imagepicker into a different view controller

I want to push a UIImagePickerViewController with a button in my parent view controller, but I want to display the image in a different view, not the parent view. I've tried
Pushing a new view and calling the image picker from a button there. However, because I have this view embedded inside a navigation view, I have a problem of 2 navigation bars. I can't hide navigation bar because then, I can't go back to the parent view.
Pushing a new view and calling the image picker directly (with no button). However, the image picker is not closing on its own and I can't go back to my parent view controller.
How about something like this:
Have an observed object to be the main object (your enviornmentObject).
Add a UIImage property to it or any property you want to share between views (after all that's the job of an environmentObject
Share the enviornmentObject
This is your class
class AppState: ObservableObject {
#Published var selectedImage: UIImage? = nil // default it to nil in case nothing is selected
}
This is your main view
struct ContentView: View {
#EnviornmentObject var appState: AppState
#State var presentModal: Bool = false
var body: some View {
VStack {
// Image can now easily be accessed by calling self.appState.selectedImage in any view that has #EnviornmentObject var appState: AppState
if(self.appState.selectedImage != nil) {
Image(uiImage: self.appState.selectedImage!)
} else {
// Image doesn't exist, add a placeholder
Text("No image selected")
}
Button("Show Modal") {
self.presentModal.toggle()
}
}.sheet(isPresented: self.$presentModal) {
ModalView(presentModal: self.$presentModal)
}
}
}
// Your ImagePicker view or any other view that will change the selected Image
struct ModalView: View {
#EnviornmentObject var appState: AppState
#Binding var presentModal: Bool = false
var body: some View {
// Your logic to pick image goes here, I will simulate a button click
Button("I will set an image") {
self.appState.selectedImage = UIImage(named: "test.jpg")
self.presentModal.toggle()
}
}
}
In your SceneDelegate (VERY IMPORTANT)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(AppState()) // <- The important part
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
Your modal view (or your image picker view, really anything)
EDIT: I agree it should be presented as a modal; however, it is not 100% necessary. Presenting it as a modal or not this should work though.
As the docs for UIUmagePickerController will tell you, it must be presented modally, not pushed. You are also responsible for dismissing it via the delegate.

How to create AppleTV buttons?

At first glance they look like regular UIButtons however they got a label below it. Also the background of the button seems to be a blurred effect.
So my thoughts are that they are put in a CollectionView (Horizontal). With each cell containing a UIButton and a UILabel. Although that may work the UIButton doesn't seem to get the move effect for free.
Is that custom behavior? And if so, how are you able to create such an effect?
I bet it is not an UICollectionView but a horizontal UIStackView of custom views in which there is a UIButton and UILabel vertically aligned.
Here you have an example, using stackViews:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.alignment = .center
stackView.spacing = 30
view.addSubview(stackView)
["One", "Two", "Three", "Caramba"].forEach {
let buttonStackView = UIStackView()
buttonStackView.axis = .vertical
buttonStackView.distribution = .fillProportionally
buttonStackView.alignment = .center
buttonStackView.spacing = 15
let button = UIButton(type: .system)
button.setTitle($0, for: .normal)
buttonStackView.addArrangedSubview(button)
let label = UILabel()
label.text = $0
label.font = UIFont.systemFont(ofSize: 20)
buttonStackView.addArrangedSubview(label)
stackView.addArrangedSubview(buttonStackView)
}
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
}
Having a custom view instead of a vertical uistackview for each button would allow to customize its layout when focused, including Parallax effect.
For adding parallax effect to each button in the stack, take a look to How to get Parallax Effect on UIButton in tvOS?

Resizing NSWindow to match view controller size in storyboard

I am working on Xcode 6.1.1 on OSX 10.10. I am trying out storyboards for Mac apps. I have a NSTabViewController using the new NSTabViewControllerTabStyleToolbar tabStyle and it is set as the default view controller for the window controller. How do I make my window resize according to the current selected view controller?
Is it possible to do entirely in Interface Builder?
Here is what my storyboard looks like:
The auto layout answer is half of it. You need to set the preferredContentSize in your ViewController for each tab to the fitting size (if you wanted the tab to size to the smallest size satisfying all constraints).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = view.fittingSize
}
If your constraints are causing an issue below try first with a fixed size, the example below sets this in the tab item's view controller's viewWillAppear function (Swift used here, but the Objective-C version works just as well).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = NSSize(width: 400, height: 280)
}
If that works, fiddle with your constraints to figure out what's going on
This solution for 'toolbar style' tab view controllers does animate and supports the nice crossfade effect. In the storyboard designer, add 'TabViewController' in the custom class name field of the NSTabViewController. Don't forget to assign a title to each viewController, this is used as a key value.
import Cocoa
class TabViewController: NSTabViewController {
private lazy var tabViewSizes: [String : NSSize] = [:]
override func viewDidLoad() {
// Add size of first tab to tabViewSizes
if let viewController = self.tabViewItems.first?.viewController, let title = viewController.title {
tabViewSizes[title] = viewController.view.frame.size
}
super.viewDidLoad()
}
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions, completionHandler completion: (() -> Void)?) {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.5
self.updateWindowFrameAnimated(viewController: toViewController)
super.transition(from: fromViewController, to: toViewController, options: [.crossfade, .allowUserInteraction], completionHandler: completion)
}, completionHandler: nil)
}
func updateWindowFrameAnimated(viewController: NSViewController) {
guard let title = viewController.title, let window = view.window else {
return
}
let contentSize: NSSize
if tabViewSizes.keys.contains(title) {
contentSize = tabViewSizes[title]!
}
else {
contentSize = viewController.view.frame.size
tabViewSizes[title] = contentSize
}
let newWindowSize = window.frameRect(forContentRect: NSRect(origin: NSPoint.zero, size: contentSize)).size
var frame = window.frame
frame.origin.y += frame.height
frame.origin.y -= newWindowSize.height
frame.size = newWindowSize
window.animator().setFrame(frame, display: false)
}
}
The window containing a toolbar style tab view controller does resize without any code if you have auto layout constraints in your storyboard tab views (macOS 11.1, Xcode 12.3). I haven't tried other style tab view controllers.
If you want to resize with animation as in Finder, it is sufficient to add one override in your tab view controller. It will resize the window with system-calculated resize animation time and will hide the tab view during resize animation:
class PreferencesTabViewController: NSTabViewController {
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Void)? = nil) {
guard let window = view.window else {
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
return
}
let fromSize = window.frame.size
let toSize = window.frameRect(forContentRect: toViewController.view.frame).size
let widthDelta = toSize.width - fromSize.width
let heightDelta = toSize.height - fromSize.height
var toOrigin = window.frame.origin
toOrigin.x += widthDelta / 2
toOrigin.y -= heightDelta
let toFrame = NSRect(origin: toOrigin, size: toSize)
NSAnimationContext.runAnimationGroup { context in
context.duration = window.animationResizeTime(toFrame)
view.isHidden = true
window.animator().setFrame(toFrame, display: false)
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
} completionHandler: { [weak self] in
self?.view.isHidden = false
}
}
}
Please adjust closure syntax if you are using Swift versions older than 5.3.
Use autolayout. Set explicit size constraints on you views. Or once you have entered the UI into each tab view item's view set up the internal constraints such that they force view to be the size you want.

How to present a modal atop the current view in Swift

(Xcode6, iOS8, Swift, iPad)
I am trying to create a classic Web-like modal view, where the outside of the dialog box is "grayed-out." To accomplish this, I've set the alpha value of the backgroundColor of the view for the modal to 0.5, like so:
self.view.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5)
The only problem is that when the modal becomes full-screen, the presenting view is removed. (Ref Transparent Modal View on Navigation Controller).
(A bit irritated at the concept here. Why remove the underlying view? A modal is, by definition, to appear atop other content. Once the underlying view is removed, it's not really a modal anymore. it's somewhere between a modal and a push transition. Wa wa wa... Anyway..)
To prevent this from happening, I've set the modalPresentationStyle to CurrentContext in the viewDidLoad method of the parent controller, and in Storyboard... but no luck.
self.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
self.navigationController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
How do I prevent the presenting view from being removed when the modal becomes full screen?
tyvm.. more info below.
Also in Storyboard, like so (Presentation: Current Context)
Thx for your help... documentation below:
First, remove all explicit setting of modal presentation style in code and do the following:
In the storyboard set the ModalViewController's modalPresentation style to Over Current context
Check the checkboxes in the Root/Presenting ViewController - Provide Context and Define Context.
They seem to be working even unchecked.
You can try this code for Swift:
let popup : PopupVC = self.storyboard?.instantiateViewControllerWithIdentifier("PopupVC") as! PopupVC
let navigationController = UINavigationController(rootViewController: popup)
navigationController.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
self.presentViewController(navigationController, animated: true, completion: nil)
For swift 4 latest syntax using extension:
extension UIViewController {
func presentOnRoot(`with` viewController : UIViewController){
let navigationController = UINavigationController(rootViewController: viewController)
navigationController.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.present(navigationController, animated: false, completion: nil)
}
}
How to use:
let popup : PopupVC = self.storyboard?.instantiateViewControllerWithIdentifier("PopupVC") as! PopupVC
self.presentOnRoot(with: popup)
The only problem I can see in your code is that you are using CurrentContext instead of OverCurrentContext.
So, replace this:
self.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
self.navigationController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
for this:
self.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
self.navigationController.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
This worked for me in Swift 5.0. Set the Storyboard Id in the identity inspector as "destinationVC".
#IBAction func buttonTapped(_ sender: Any) {
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let destVC = storyboard.instantiateViewController(withIdentifier: "destinationVC") as! MyViewController
destVC.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
destVC.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(destVC, animated: true, completion: nil)
}
The problem with setting the modalPresentationStyle from code was that you should have set it in the init() method of the presented view controller, not the parent view controller.
From UIKit docs: "Defines the transition style that will be used for this view controller when it is presented modally. Set
this property on the view controller to be presented, not the presenter. Defaults to
UIModalTransitionStyleCoverVertical."
The viewDidLoad method will only be called after you already presented the view controller.
The second problem was that you should use UIModalPresentationStyle.overCurrentContext.
The only way I able to get this to work was by doing this on the presenting view controller:
func didTapButton() {
self.definesPresentationContext = true
self.modalTransitionStyle = .crossDissolve
let yourVC = self.storyboard?.instantiateViewController(withIdentifier: "YourViewController") as! YourViewController
let navController = UINavigationController(rootViewController: yourVC)
navController.modalPresentationStyle = .overCurrentContext
navController.modalTransitionStyle = .crossDissolve
self.present(navController, animated: true, completion: nil)
}
I am updating a simple solution. First add an id to your segue which presents modal. Than in properties change it's presentation style to "Over Current Context". Than add this code in presenting view controller (The controller which is presenting modal).
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let Device = UIDevice.currentDevice()
let iosVersion = NSString(string: Device.systemVersion).doubleValue
let iOS8 = iosVersion >= 8
let iOS7 = iosVersion >= 7 && iosVersion < 8
if((segue.identifier == "chatTable")){
if (iOS8){
}
else {
self.navigationController?.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
}
}
}
Make sure you change segue.identifier to your own id ;)

Resources