LargeTitles UIScrollView does not support multiple observers implementing _scrollViewWillEndDraggingWithVelocity:targetContentOffset - swift

I have implemented large titles in my app with the following code:
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .always
} else {
// Fallback on earlier versions
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y <= 0 {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .always
} else {
// Fallback on earlier versions
}
} else {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .never
} else {
// Fallback on earlier versions
}
}
self.navigationController?.navigationBar.setNeedsLayout()
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.01, animations: {
self.navigationController?.navigationBar.layoutIfNeeded()
self.view.layoutIfNeeded()
})
}
I am able to successfully toggle between views on a tabbar but when I push a view ontop of the tabbar controller and then pop it off using this code:
_ = self.navigationController?.popViewController(animated: true)
I get this crash when I toggle between views on the tabbar again:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'ERROR: UIScrollView does not support multiple observers implementing _scrollViewWillEndDraggingWithVelocity:targetContentOffset:'

This is not a solution, but a potential thing that you need to investigate in your code. I got this same error message (UIScrollView does not support multiple observers implementing _scrollViewWillEndDraggingWithVelocity:targetContentOffset) and I noticed I was doing something incorrectly.
I got this error message in a SwiftUI app using NavigationView.
The mistake I had made was that ParentView had a Navigation View at the root. Using a NavigationLink I was moving to ChildView, which also had a NavigationView as the root. Here's what it looked like in code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ParentView()
}
}
}
struct ParentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: ChildView()) {
Text("Parent view")
}
}
.navigationTitle("Parent")
}
}
}
struct ChildView: View {
var body: some View {
List {
ForEach(0 ..< 5) { _ in
Text("Child view")
}
}
.navigationTitle("Child")
}
}
Initially this is what ChildView looked like:
struct ChildView: View {
var body: some View {
NavigationView {
List {
ForEach(0 ..< 5) { _ in
Text("Second screen")
}
}
.navigationTitle("Second")
}
}
}
Notice how I was trying to push a view which itself was embedded in a NavigationView. Removing it as shown in the first snippet, took care of the error messages. You can try looking into that, maybe you are doing the same mistake just in UIKit instead of SwiftUI.

I found the solution. You have to set the first navigation controller to not use large titles.
The point is that now UIScrollView has only one observer (navigationController) implementing _scrollViewWillEndDraggingWithVelocity.
if (#available(iOS 11.0, *)) {
self.navigationController.navigationBar.prefersLargeTitles = FALSE;
self.navigationController.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever;
}

The problem happened when the tableview was still scrolling when I went to another view. I fixed the problem by setting a bool in the scrollViewDidScroll that disables any scrolling when the segue is started.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if viewIsVisible {
if scrollView.contentOffset.y <= 0 {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .always
} else {
// Fallback on earlier versions
}
} else {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .never
} else {
// Fallback on earlier versions
}
}
self.navigationController?.navigationBar.setNeedsLayout()
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.01, animations: {
self.navigationController?.navigationBar.layoutIfNeeded()
self.view.layoutIfNeeded()
})
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
self.viewIsVisible = false
}

I've the same problem and I fixed it by removing this line from AppDelegate:
UINavigationBar.appearance().prefersLargeTitles = true
and handle prefersLargeTitles inside viewDidLoad in certain UIViewController

I think all of above answers don't really solve the issue and are overcomplicated. I recommend enabling/disabling large titles in each of your UIViewController's subclasses, so they don't use large titles at the same time. Good place to do it is in the viewWillAppear and viewWillDisappear methods
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.largeTitleDisplayMode = .always
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.prefersLargeTitles = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.prefersLargeTitles = false
}

Related

Adding a view modifier inside .onChange

Is it possible to add a view modifier inside .onChange?
Simplified example:
content
.onChange(of: publishedValue) {
content.foregroundColor(.red)
}
I have a theme that when changed needs to change the status bar color. I have a view modifier created for that ( https://barstool.engineering/set-the-ios-status-bar-style-in-swiftui-using-a-custom-view-modifier/ ). The modifier works fine, but I need to update it as the publishedValue changes.
Actual minimal example:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: TestViewModel
var body: some View {
ZStack {
Rectangle().foregroundColor(.mint)
VStack(alignment: .center, spacing: 25) {
Text("Test text \(viewModel.publishedValue)")
.onChange(of: viewModel.publishedValue) { newValue in
// Change status bar color
if viewModel.publishedValue % 2 == 0 {
self.body.statusBarStyle(.lightContent)
} else {
self.body.statusBarStyle(.darkContent)
}
}
Button("Increment") {
viewModel.publishedValue += 1
}
}
}
.ignoresSafeArea()
.statusBarStyle(.lightContent)
}
}
class TestViewModel: ObservableObject {
#Published var publishedValue: Int
init(publishedValue: Int) {
self.publishedValue = publishedValue
}
}
extension View {
/// Overrides the default status bar style with the given `UIStatusBarStyle`.
///
/// - Parameters:
/// - style: The `UIStatusBarStyle` to be used.
func statusBarStyle(_ style: UIStatusBarStyle) -> some View {
return self.background(HostingWindowFinder(callback: { window in
guard let rootViewController = window?.rootViewController else { return }
let hostingController = HostingViewController(rootViewController: rootViewController, style: style)
window?.rootViewController = hostingController
}))
}
}
fileprivate class HostingViewController: UIViewController {
private var rootViewController: UIViewController?
private var style: UIStatusBarStyle = .default
init(rootViewController: UIViewController, style: UIStatusBarStyle) {
self.rootViewController = rootViewController
self.style = style
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let child = rootViewController else { return }
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return style
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setNeedsStatusBarAppearanceUpdate()
}
}
fileprivate struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
// ...
}
}
GitHub repo for the example project: https://github.com/Iikeli/view-modifier-test
You are way overcomplicating this. Since your viewModel is #ObservedObject and the publishedValue is Published, the body of your View will be recalculated automatically every time publishedValue is updated. There's no need for a manual onChange.
You can simply move the logic into the input argument of statusBarStyle.
var body: some View {
ZStack {
Rectangle().foregroundColor(.mint)
VStack(alignment: .center, spacing: 25) {
Text("Test text \(viewModel.publishedValue)")
Button("Increment") {
viewModel.publishedValue += 1
}
}
}
.ignoresSafeArea()
.statusBarStyle(viewModel.publishedValue % 2 == 0 ? .lightContent : .darkContent)
}
Or even better, move the logic into a separate computed property:
var body: some View {
....
.statusBarStyle(statusBarStyle)
}
private var statusBarStyle: UIStatusBarStyle {
viewModel.publishedValue % 2 == 0 ? .lightContent : .darkContent
}
The short answer is no, but that doesn't mean you can't use it to have views change based on some .onChange(..) action. For example.
#State var somethingChanged = false
Text(somethingChanged ? "First Value" : "Second Value")
// Your code/view
.onChange(..) {
//Some Condition or whatever you want.
somethingChanged = true
}
Your usage might look something like this.
content
.foregroundColor(somethingChanged ? .red : .blue)
.onChange(ofPublishedValue) {
somethingChanged = true
}
First of all, thanks for the help. Neither of the answers helped in my situation, since I couldn't get the modifier to update with the variable change. But with some Googling and trying out different solutions I figured out a working solution for updating the status bar colors.
I needed to update the style variable in the HostingViewController, and then update accordingly. So I added the HostingViewController as a #StateObject and updated the style variable inside the .onChange(). Not quite the solution I was going with to start out, but it does work.
The code:
import SwiftUI
import Introspect
struct ContentView: View {
#ObservedObject var viewModel: TestViewModel
#StateObject var hostingViewController: HostingViewController = .init(rootViewController: nil, style: .default)
var body: some View {
ZStack {
Rectangle().foregroundColor(.mint)
VStack(alignment: .center, spacing: 25) {
Text("Test text \(viewModel.publishedValue)")
.onChange(of: viewModel.publishedValue) { newValue in
// Change status bar color
if viewModel.publishedValue % 2 == 0 {
hostingViewController.style = .lightContent
} else {
hostingViewController.style = .darkContent
}
}
Button("Increment") {
viewModel.publishedValue += 1
}
}
}
.ignoresSafeArea()
.introspectViewController { viewController in
let window = viewController.view.window
guard let rootViewController = window?.rootViewController else { return }
hostingViewController.rootViewController = rootViewController
window?.rootViewController = hostingViewController
}
}
}
class TestViewModel: ObservableObject {
#Published var publishedValue: Int
init(publishedValue: Int) {
self.publishedValue = publishedValue
}
}
class HostingViewController: UIViewController, ObservableObject {
var rootViewController: UIViewController?
var style: UIStatusBarStyle = .default {
didSet {
self.rootViewController?.setNeedsStatusBarAppearanceUpdate()
}
}
init(rootViewController: UIViewController?, style: UIStatusBarStyle) {
self.rootViewController = rootViewController
self.style = style
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let child = rootViewController else { return }
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return style
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setNeedsStatusBarAppearanceUpdate()
}
}
Big shoutout to this answer for giving me all that I needed to implement the solution. Note: You can override the rootViewController however you like, I used SwiftUI-Introspect as we are already using it in our project anyway.
GitHub branch

SwiftUI remove transition of fullscreen cover

Is there any way in how to remove the animation/transition of the fullscreen cover?
This is my code:
let contentView = UIHostingController(rootView: ContentView())
override func viewDidLoad() {
super.viewDidLoad()
configureBackgroundGradient()
addChild(contentView)
view.addSubview(contentView.view)
setupContraints()
}
struct ContentView: View {
var body: some View {
EmptyView().fullScreenCover(isPresented: /*#START_MENU_TOKEN#*/.constant(true)/*#END_MENU_TOKEN#*/, content: {
FullScreenView.init()
})
}
Thanks for your help!
You can use withTransaction to diable the transition animation between views
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
self.isPresentedViewB = true
}
https://developer.apple.com/documentation/swiftui/transaction
Use open class func setAnimationsEnabled(_ enabled: Bool) for disabling animation for the whole app.
In your case, you just needed to disable animation during the fullscreen present and start again after the present.
Here is the possible solution
override func viewDidLoad() {
super.viewDidLoad()
configureBackgroundGradient()
UIView.setAnimationsEnabled(false) //<== Disable animation for whole app
addChild(contentView)
view.addSubview(contentView.view)
DispatchQueue.main.async {
UIView.setAnimationsEnabled(true) //<== Again enable animation for whole app
}
setupContraints()
}
You can also write this inside the ContentView instead of viewDidLoad
struct ContentView: View {
#State private var isPresent: Bool = false{
willSet {
UIView.setAnimationsEnabled(false) //<== Disable animation for whole app
} didSet {
DispatchQueue.main.async {
UIView.setAnimationsEnabled(true) //<== Again enable animation for whole app
}
}
}
var body: some View {
EmptyView().fullScreenCover(isPresented: $isPresent, content: {
FullScreenView.init()
})
.onAppear() {
isPresent = true
}
}
}
You can also use this extension.
extension View {
func withoutAnimation(_ work: #escaping () -> Void) {
UIView.setAnimationsEnabled(false) //<== Disable animation for whole app
work()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
UIView.setAnimationsEnabled(true) //<== Again enable animation for whole app
}
}
}
usage:
struct ContentView: View {
#State private var isPresent: Bool = false
var body: some View {
EmptyView().fullScreenCover(isPresented: $isPresent, content: {
FullScreenView.init()
})
.onAppear() {
withoutAnimation {
isPresent = true
}
}
}
}

SwiftUI sheet() modals with custom size on iPad

How can I control the preferred presentation size of a modal sheet on iPad with SwiftUI? I'm surprised how hard it is to find an answer on Google for this.
Also, what is the best way to know if the modal is dismissed by dragging it down (cancelled) or actually performing a custom positive action?
Here is my solution for showing a form sheet on an iPad in SwiftUI:
struct MyView: View {
#State var show = false
var body: some View {
Button("Open Sheet") { self.show = true }
.formSheet(isPresented: $show) {
Text("Form Sheet Content")
}
}
}
Enabled by this UIViewControllerRepresentable
class FormSheetWrapper<Content: View>: UIViewController, UIPopoverPresentationControllerDelegate {
var content: () -> Content
var onDismiss: (() -> Void)?
private var hostVC: UIHostingController<Content>?
required init?(coder: NSCoder) { fatalError("") }
init(content: #escaping () -> Content) {
self.content = content
super.init(nibName: nil, bundle: nil)
}
func show() {
guard hostVC == nil else { return }
let vc = UIHostingController(rootView: content())
vc.view.sizeToFit()
vc.preferredContentSize = vc.view.bounds.size
vc.modalPresentationStyle = .formSheet
vc.presentationController?.delegate = self
hostVC = vc
self.present(vc, animated: true, completion: nil)
}
func hide() {
guard let vc = self.hostVC, !vc.isBeingDismissed else { return }
dismiss(animated: true, completion: nil)
hostVC = nil
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
hostVC = nil
self.onDismiss?()
}
}
struct FormSheet<Content: View> : UIViewControllerRepresentable {
#Binding var show: Bool
let content: () -> Content
func makeUIViewController(context: UIViewControllerRepresentableContext<FormSheet<Content>>) -> FormSheetWrapper<Content> {
let vc = FormSheetWrapper(content: content)
vc.onDismiss = { self.show = false }
return vc
}
func updateUIViewController(_ uiViewController: FormSheetWrapper<Content>,
context: UIViewControllerRepresentableContext<FormSheet<Content>>) {
if show {
uiViewController.show()
}
else {
uiViewController.hide()
}
}
}
extension View {
public func formSheet<Content: View>(isPresented: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content) -> some View {
self.background(FormSheet(show: isPresented,
content: content))
}
}
You should be able to modify the code in func show() according to UIKit specs in order to get the sizing the way you like (and you can even go so far as to inject parameters from the SwiftUI side if needed). This is just how I get a form sheet to work on iPad as .sheet was just too big for my use case
I just posted the same in this SO How can I make a background color with opacity on a Sheet view?
but it seems to do exactly what I need it to do. It makes the background of the sheet transparent while allowing the content to be sized as needed to appear as if it's the only part of the sheet. Works great on the iPad.
Using the AWESOME answer from #Asperi that I have been trying to find all day, I have built a simple view modifier that can now be applied inside a .sheet or .fullScreenCover modal view and provides a transparent background. You can then set the frame modifier for the content as needed to fit the screen without the user having to know the modal is not custom sized.
import SwiftUI
struct ClearBackgroundView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
let view = UIView()
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
struct ClearBackgroundViewModifier: ViewModifier {
func body(content: Content) -> some View {
content
.background(ClearBackgroundView())
}
}
extension View {
func clearModalBackground()->some View {
self.modifier(ClearBackgroundViewModifier())
}
}
Usage:
.sheet(isPresented: $isPresented) {
ContentToDisplay()
.frame(width: 300, height: 400)
.clearModalBackground()
}
In case it helps anyone else, I was able to get this working by leaning on this code to hold the view controller:
https://gist.github.com/timothycosta/a43dfe25f1d8a37c71341a1ebaf82213
struct ViewControllerHolder {
weak var value: UIViewController?
init(_ value: UIViewController?) {
self.value = value
}
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder? { ViewControllerHolder(UIApplication.shared.windows.first?.rootViewController) }
}
extension EnvironmentValues {
var viewController: ViewControllerHolder? {
get { self[ViewControllerKey.self] }
set { self[ViewControllerKey.self] = newValue }
}
}
extension UIViewController {
func present<Content: View>(
presentationStyle: UIModalPresentationStyle = .automatic,
transitionStyle _: UIModalTransitionStyle = .coverVertical,
animated: Bool = true,
completion: #escaping () -> Void = { /* nothing by default*/ },
#ViewBuilder builder: () -> Content
) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = presentationStyle
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, ViewControllerHolder(toPresent))
)
if presentationStyle == .overCurrentContext {
toPresent.view.backgroundColor = .clear
}
present(toPresent, animated: animated, completion: completion)
}
}
Coupled with a specialized view to handle common elements in the modal:
struct ModalContentView<Content>: View where Content: View {
// Use this function to provide the content to display and to bring up the modal.
// Currently only the 'formSheet' style has been tested but it should work with any
// modal presentation style from UIKit.
public static func present(_ content: Content, style: UIModalPresentationStyle = .formSheet) {
let modal = ModalContentView(content: content)
// Present ourselves
modal.viewController?.present(presentationStyle: style) {
modal.body
}
}
// Grab the view controller out of the environment.
#Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder?
private var viewController: UIViewController? {
viewControllerHolder?.value
}
// The content to be displayed in the view.
private var content: Content
public var body: some View {
VStack {
/// Some specialized controls, like X button to close omitted...
self.content
}
}
Finally, simply call:
ModalContentView.present( MyAwesomeView() )
to display MyAwesomeView inside of a .formSheet modal.
There are some issues in #ccwasden's answer. Dismissing popover won't change $isPresented all the time, as delegate is not set and hostVC is never assigned.
Here are some modifications required.
In FlexSheetWrapper:
func show() {
guard hostVC == nil else { return }
let vc = UIHostingController(rootView: content())
vc.view.sizeToFit()
vc.preferredContentSize = vc.view.bounds.size
vc.modalPresentationStyle = .formSheet
vc.presentationController?.delegate = self
hostVC = vc
self.present(vc, animated: true, completion: nil)
}
And in FormSheet:
func updateUIViewController(_ uiViewController: FlexSheetWrapper<Content>,
context: UIViewControllerRepresentableContext<FlexSheet<Content>>) {
if show {
uiViewController.show()
}
else {
uiViewController.hide()
}
}
From #ccwasden answer, I fixed the problem when you $isPresented = true at the beginning of code, the modal will not present when the view is loaded, To do so here is code View+FormSheet.swift
Result
// You can now set `test = true` at first
.formSheet(isPresented: $test) {
Text("Hi")
}
View+FormSheet.swift
import SwiftUI
class ModalUIHostingController<Content>: UIHostingController<Content>, UIPopoverPresentationControllerDelegate where Content : View {
var onDismiss: (() -> Void)
required init?(coder: NSCoder) { fatalError("") }
init(onDismiss: #escaping () -> Void, rootView: Content) {
self.onDismiss = onDismiss
super.init(rootView: rootView)
view.sizeToFit()
preferredContentSize = view.bounds.size
modalPresentationStyle = .formSheet
presentationController?.delegate = self
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
print("modal dismiss")
onDismiss()
}
}
class ModalUIViewController<Content: View>: UIViewController {
var isPresented: Bool
var content: () -> Content
var onDismiss: (() -> Void)
private var hostVC: ModalUIHostingController<Content>
private var isViewDidAppear = false
required init?(coder: NSCoder) { fatalError("") }
init(isPresented: Bool = false, onDismiss: #escaping () -> Void, content: #escaping () -> Content) {
self.isPresented = isPresented
self.onDismiss = onDismiss
self.content = content
self.hostVC = ModalUIHostingController(onDismiss: onDismiss, rootView: content())
super.init(nibName: nil, bundle: nil)
}
func show() {
guard isViewDidAppear else { return }
self.hostVC = ModalUIHostingController(onDismiss: onDismiss, rootView: content())
present(hostVC, animated: true)
}
func hide() {
guard !hostVC.isBeingDismissed else { return }
dismiss(animated: true)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
isViewDidAppear = true
if isPresented {
show()
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
isViewDidAppear = false
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
show()
}
}
struct FormSheet<Content: View> : UIViewControllerRepresentable {
#Binding var show: Bool
let content: () -> Content
func makeUIViewController(context: UIViewControllerRepresentableContext<FormSheet<Content>>) -> ModalUIViewController<Content> {
let onDismiss = {
self.show = false
}
let vc = ModalUIViewController(isPresented: show, onDismiss: onDismiss, content: content)
return vc
}
func updateUIViewController(_ uiViewController: ModalUIViewController<Content>,
context: UIViewControllerRepresentableContext<FormSheet<Content>>) {
if show {
uiViewController.show()
}
else {
uiViewController.hide()
}
}
}
extension View {
public func formSheet<Content: View>(isPresented: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content) -> some View {
self.background(FormSheet(show: isPresented,
content: content))
}
}

Conforming to protocol but nothing happens

I have 3 UIViewControllers. ContainerVC which contains 2 ContainerViews. First Container View is DashboardVC and second one is SidebarVC. The DashboardVC covers the entire screen, while the SidebarVC is outside.
I have a leading constraint for the SidebarVC that should be animated and the SidebarVC should slide in (from the left side). On the DashboardVC I have a UIBarButtonItem and when it's pressed it should perform the animation. The problem is that I'm doing something wrong with the delegate and when the ContainerVC conforms to the protocol, nothing happens.
PS: I have very hard time understanding protocols/delegates despite having watch a bunch of different videos on this concept. Here's the code:
DashboardVC
protocol SideBarDelegate {
func showMenu()
func hideMenu()
}
class DashboardVC: UIViewController {
var delegate: SideBarDelegate?
var isSideMenuOpen = true
#IBAction func menuButtonPressed(_ sender: UIBarButtonItem) {
if isSideMenuOpen {
delegate?.showMenu()
isSideMenuOpen = false
}
else {
delegate?.hideMenu()
isSideMenuOpen = true
}
}
}
ContainerVC
class ContainerVC: UIViewController {
#IBOutlet weak var sideBarMenuLeadingConstraint: NSLayoutConstraint!
}
extension ContainerVC : SideBarDelegate {
func showMenu() {
sideBarMenuLeadingConstraint.constant = -290
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
func hideMenu() {
sideBarMenuLeadingConstraint.constant = 0
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
You use the delegate only on classes. To prevent memory leaks, do those two things:
Change:
protocol SideBarDelegate {
func showMenu()
func hideMenu()
}
to:
protocol SideBarDelegate: class {
func showMenu()
func hideMenu()
}
Now, rename delegate property to:
weak var delegate: SideBarDelegate?
Weak does not increase the reference counting. This is important to prevent memory leaks.
Your instance of ContainerVC must have some sort of reference to an instance of DashboardVC (or make the delegate static but I have never seen something like that). Then, in your viewDidLoad method of ContainterVC, set this:
myInstanceReferenceToDashboardVC.delegate = self

How to override trait collection for initial UIViewController? (with Storyboard)

I have an app targeted iOS8 and initial view controller is UISplitViewController. I use storyboard, so that it kindly instantiate everything for me.
Because of my design I need SplitViewController to show both master and detail views in portrait mode on iPhone. So I am looking for a way to override trait collection for this UISplitViewController.
I found that I can use
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) { ... }
but, unfortunately, there are only methods to override child controllers traits collections:
setOverrideTraitCollection(collection: UITraitCollection!, forChildViewController childViewController: UIViewController!)
and I can't do so for self in my UISplitViewController subclass.
I checked an example app Adaptive Photos from Apple. And in this app author use special TraitOverrideViewController as root and some magic in his viewController setter to make it all works.
It looks horrible for me. Is there are any way around to override traits? Or If there are not, how can I manage to use the same hack with storyboard? In other words, how to inject some viewController as root one only to handle traits for my UISplitViewController with storyboard?
Ok, I wish there was another way around this, but for now I just converted code from the Apple example to Swift and adjusted it to use with Storyboards.
It works, but I still believe it is an awful way to archive this goal.
My TraitOverride.swift:
import UIKit
class TraitOverride: UIViewController {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var forcedTraitCollection: UITraitCollection? {
didSet {
updateForcedTraitCollection()
}
}
override func viewDidLoad() {
setForcedTraitForSize(view.bounds.size)
}
var viewController: UIViewController? {
willSet {
if let previousVC = viewController {
if newValue !== previousVC {
previousVC.willMoveToParentViewController(nil)
setOverrideTraitCollection(nil, forChildViewController: previousVC)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
}
}
}
didSet {
if let vc = viewController {
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMoveToParentViewController(self)
updateForcedTraitCollection()
}
}
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) {
setForcedTraitForSize(size)
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
}
func setForcedTraitForSize (size: CGSize) {
let device = traitCollection.userInterfaceIdiom
var portrait: Bool {
if device == .Phone {
return size.width > 320
} else {
return size.width > 768
}
}
switch (device, portrait) {
case (.Phone, true):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Regular)
case (.Pad, false):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Compact)
default:
forcedTraitCollection = nil
}
}
func updateForcedTraitCollection() {
if let vc = viewController {
setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
performSegueWithIdentifier("toSplitVC", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if segue.identifier == "toSplitVC" {
let destinationVC = segue.destinationViewController as UIViewController
viewController = destinationVC
}
}
override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
return true
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return true
}
}
To make it work you need to add a new UIViewController on the storyboard and made it the initial. Add show segue from it to your real controller like this:
You need to name the segue "toSplitVC":
and set initial controller to be TraitOverride:
Now it should work for you too. Let me know if you find a better way or any flaws in this one.
I understand that you wanted a SWIFT translation here... And you've probably solved that.
Below is something I've spent a considerable time trying to resolve - getting my SplitView to work on an iPhone 6+ - this is a Cocoa solution.
My Application is TabBar based and the SplitView has Navigation Controllers. In the end my issue was that setOverrideTraitCollection was not being sent to the correct target.
#interface myUITabBarController ()
#property (nonatomic, retain) UITraitCollection *overrideTraitCollection;
#end
#implementation myUITabBarController
- (void)viewDidLoad
{
[super viewDidLoad];
[self performTraitCollectionOverrideForSize:self.view.bounds.size];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
NSLog(#"myUITabBarController %#", NSStringFromSelector(_cmd));
[self performTraitCollectionOverrideForSize:size];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
- (void)performTraitCollectionOverrideForSize:(CGSize)size
{
NSLog(#"myUITabBarController %#", NSStringFromSelector(_cmd));
_overrideTraitCollection = nil;
if (size.width > 320.0)
{
_overrideTraitCollection = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
[self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:self];
for (UIViewController * view in self.childViewControllers)
{
[self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:view];
NSLog(#"myUITabBarController %# AFTER viewTrait=%#", NSStringFromSelector(_cmd), [view traitCollection]);
}
}
#end
UPDATE:
Apple do not recommend doing this:
Use the traitCollection property directly. Do not override it. Do not
provide a custom implementation.
I'm not overriding this property anymore! Now I'm calling overrideTraitCollectionForChildViewController: in the parent viewControler class.
Old answer:
I know it's more than a year since question was asked, but i think my answer will help someone like me who do not achieved success with the accepted answer.
Whell the solution is really simple, you can just override traitCollection: method. Here is an example from my app:
- (UITraitCollection *)traitCollection {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
return super.traitCollection;
} else {
switch (self.modalPresentationStyle) {
case UIModalPresentationFormSheet:
case UIModalPresentationPopover:
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
default:
return super.traitCollection;
}
}
}
the idea is to force Compact size class on iPad if controller is presented as popover or form sheet.
Hope it helps.
The extra top level VC works well for a simple app but it won't propagate down to modally presented VC's as they don't have a parentVC. So you need to insert it again in different places.
A better approach I found was just to subclass UINavigationController and then just use your subclass in the storyboard and elsewhere where you would normally use UINavigationController. It saves the additional VC clutter in storyboards and also saves extra clutter in code.
This example will make all iPhones use regular horizontal size class for landscape.
#implementation MyNavigationController
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
UIDevice *device = [UIDevice currentDevice];
if (device.userInterfaceIdiom == UIUserInterfaceIdiomPhone && CGRectGetWidth(childViewController.view.bounds) > CGRectGetHeight(childViewController.view.bounds)) {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
return nil;
}
#end
Yes, it must use custom container View Controller to override the function viewWillTransitionToSize. You use the storyboard to set the container View Controller as initial.
Also, you can refer this good artical which use the program to implement it. According to it, your judgement portait could have some limitations:
var portrait: Bool {
if device == .Phone {
return size.width > 320
} else {
return size.width > 768
}
}
other than
if **size.width > size.height**{
self.setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClass.Regular), forChildViewController: viewController)
}
else{
self.setOverrideTraitCollection(nil, forChildViewController: viewController)
}
"
Props To #Ilyca
Swift 3
import UIKit
class TraitOverride: UIViewController {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
var forcedTraitCollection: UITraitCollection? {
didSet {
updateForcedTraitCollection()
}
}
override func viewDidLoad() {
setForcedTraitForSize(size: view.bounds.size)
}
var viewController: UIViewController? {
willSet {
if let previousVC = viewController {
if newValue !== previousVC {
previousVC.willMove(toParentViewController: nil)
setOverrideTraitCollection(nil, forChildViewController: previousVC)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
}
}
}
didSet {
if let vc = viewController {
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
updateForcedTraitCollection()
}
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
setForcedTraitForSize(size: size)
super.viewWillTransition(to: size, with: coordinator)
}
func setForcedTraitForSize (size: CGSize) {
let device = traitCollection.userInterfaceIdiom
var portrait: Bool {
if device == .phone {
return size.width > 320
} else {
return size.width > 768
}
}
switch (device, portrait) {
case (.phone, true):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .regular)
case (.pad, false):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .compact)
default:
forcedTraitCollection = nil
}
}
func updateForcedTraitCollection() {
if let vc = viewController {
setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
performSegue(withIdentifier: "toSplitVC", sender: self)
}
override var shouldAutomaticallyForwardAppearanceMethods: Bool {
return true
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return true
}
}