Cosmicmind/Material: How do i implement PageTabBarController without using AppDelegate.swift? - swift3

as we know, to implement PageTabBarController, we need to insert these code in AppDelegate.swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:
let viewControllers = [MatchDetailViewController(),ListPlayersViewController(),ChatViewController()]
window = UIWindow(frame: Device.bounds)
window!.rootViewController = MatchViewController(viewControllers: viewControllers, selectedIndex: 0)
window!.makeKeyAndVisible()
}
Now, i need to use PageTabBarController when i want to open detail for my match data. My question is, how to implement it without insert those code in AppDelegate.swift because it will open my MatchViewController (extend from PageTabBarController) for the first app launch.
I have tried this code, but it will cause Crash, and it pointed to my AppDelegate.swift
class MatchViewController: PageTabBarController {
var window: UIWindow?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
open override func prepare() {
super.prepare()
let viewControllers = [MatchDetailViewController(),ListPlayersViewController(),ChatViewController()]
//1st try: Crash
window = UIWindow(frame: Device.bounds)
window!.rootViewController = MatchViewController(viewControllers: viewControllers, selectedIndex: 0)
window!.makeKeyAndVisible()
//2nd try: error
self.rootViewController = MatchViewController(viewControllers: viewControllers, selectedIndex: 0)
//3rd try: crash
self.viewControllers = viewControllers
delegate = self
preparePageTabBar()
}
fileprivate func preparePageTabBar() {
pageTabBar.lineColor = Color.blue.base
pageTabBar.dividerColor = Color.blueGrey.lighten5
pageTabBarAlignment = PageTabBarAlignment.top
pageTabBar.lineAlignment = TabBarLineAlignment.bottom
}
}
extension MatchViewController: PageTabBarControllerDelegate {
func pageTabBarController(_ pageTabBarController: PageTabBarController, didTransitionTo viewController: UIViewController) {
}
}

Linked GitHub Question
Hi, yes there is a way. The PageTabBarController is inherited from aUIViewController`, which allows you to add it as a child of any other UIViewController. That said, you just gave me a great idea. I am going to make a new UIViewController that allows you to add as many child UIViewControllers, which will make this super easy to do. I will make this as a Feature Request.
Until the update, please use the suggested method of adding it as a child UIViewController. Are you familiar with how to do that?

First create AppToolbarController (subclass of ToolbarController) or you can use the one in the Material library demo.
And then from your view controller, you can use:
DispatchQueue.main.async {
let tabbarViewController = AppPageTabbarController(viewControllers: [vc1,vc2,vc3], selectedIndex: 0)
self.present(AppToolbarController(rootViewontroller: tabbarViewController))
}

Related

XCTest VC Hierarchy without XCUITest?

I want to test a ViewController hierarchy without using XCUITests, because they take a huge ton longer than plain old XCTests.
I've had some success but I can't seem to create a ViewController hierarchy more than one level deep.
Here's a test case demonstrating the issue:
class VCHierarchyTests: XCTestCase {
func test_CanMakeViewControllerHierarchy() {
let baseVC = UIViewController()
let vcToPresent = UIViewController()
let vcOnTopOfThat = UIViewController()
let vcOnTopOfThatOnTopOfThat = UIViewController()
newKeyWindowWith(root: baseVC)
baseVC.present(vcToPresent, animated: false)
vcToPresent.present(vcOnTopOfThat, animated: false)
vcOnTopOfThat.present(vcOnTopOfThatOnTopOfThat, animated: false)
//this passes:
XCTAssertNotNil(baseVC.presentedViewController)
//this fails:
XCTAssertNotNil(vcToPresent.presentedViewController)
//this fails:
XCTAssertNotNil(vcOnTopOfThat.presentedViewController)
}
private func newKeyWindowWith(root viewController: UIViewController){
let window = UIWindow(frame: UIScreen.main.bounds)
window.makeKeyAndVisible()
window.rootViewController = viewController
}
}
Is there a way to construct ViewController hierarchies without XCUITest?
Try PresentationVerifier from ViewControllerPresentationSpy to unit test how view controllers are presented.
It's designed to intercept a single present() call, not several of them. If your use case falls outside what it does, please file an Issue.

SwiftUI How to share a URL to Facebook using FBSDKShareKit and ShareDialog?

I tried looking for a solution in posts such as this and this where people asked this very same question: How to share a url to Facebook using SwiftUI?
I even tried this post where somebody asked how to export a file using SwiftUI, but my problem is specifically with Facebook since I have no problem sharing urls to apps such as Whatsapp.
However, I found no answers...
UIKit
By reading Facebook's Developer documentation, I found a way to share a post using UIKit.
I created a very simple sample project to make sure I understood the topic.
Here is the sample project in UIKit:
import UIKit
import FBSDKShareKit
class ProfileViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func share(_ sender: UIButton) {
shareLink(url: URL(string: "http://www.apple.com")!)
}
func shareLink(url: URL) {
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(
fromViewController: self,
content: content,
delegate: nil
)
dialog.show()
}
}
Luckily, this worked right away.
By making this project work, I confirmed that I wasn't forgetting to link my AppBundleID to Facebook, or forgetting to add my FacebookAppID or any other needed files inside my info.plist.
SwiftUI
I then tried to replicate the same project in SwiftUI to see if I could get it to work.
I used ViewControllerRepresentable to be able to include UIActivityViewController into SwiftUI.
The reason for using UIActivityViewController is because I want the user to choose where they want to share the URL (Whatsapp, Twitter, Facebook, etc).
Here is the code:
ContentView
struct ContentView: View {
#State var showSharingView = false
var body: some View {
Button("Share Link") {
showSharingView.toggle()
}
.sheet(isPresented: $showSharingView) {
ActivityViewController(activityItems: [URL(string: "https://www.apple.com/")!])
}
}
}
UIViewControllerRepresentable
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [URL]
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
if activityType == .postToFacebook {
shareLink(from: activityItems.first!)
}
}
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
func shareLink(from url: URL) {
// controller was created so I would have a UIViewControllerType to put as a parameter for fromViewController in ShareDialog, even though I don't think it fits
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(fromViewController: controller, content: content, delegate: nil)
dialog.show()
}
}
This code doesn't work because of what I assume is the controller variable used when initializing ShareDialog. (self doesn't work either because "XCode cannot convert value of type 'ActivityViewController' to expected argument type 'UIViewController?'")
Question
Facebook Developer's documentation tells me that I need to write the following code in order to share a link:
guard let url = URL(string: "https://developers.facebook.com") else {
// handle and return
}
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(
viewController: self, //<--this has been changed to 'fromViewController'
content: content,
delegate: self
)
dialog.show()
However, SwiftUI doesn't work with ViewControllers. How can I create a ViewController to use as a parameter in fromViewController in ShareDialog in order to successfully share my URL to Facebook?
Use like this,
guard let url = URL(string: "https://developers.facebook.com") else {
// handle and return
}
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(
viewController: UIApplication.shared.windows.first!.rootViewController,
content: content,
delegate: UIApplication.shared.windows.first!.rootViewController
)
dialog.show()

Is there a reason a UIViewControllerRepresentable should never be a class?

Let's say that you don't really need SwiftUI features. I.e. you don't have import SwiftUI in your file. Instead, you only require
import protocol SwiftUI.UIViewControllerRepresentable
In general, you're going to have to involve a delegate object: an AnyObject at best, and usually, because the UIKit APIs are old, an NSObject.
The common pattern is to use a Coordinator class for that, and have the View itself be a struct, but is there always point in that indirection?
Here's an example which hasn't given me any trouble in practice:
import Combine
import MultipeerConnectivity
import protocol SwiftUI.UIViewControllerRepresentable
extension MCBrowserViewController {
final class View: NSObject {
init(
serviceType: String,
session: MCSession,
peerCountRange: ClosedRange<Int>? = nil
) {
self.serviceType = serviceType
self.session = session
self.peerCountRange = peerCountRange
}
private let serviceType: String
private unowned let session: MCSession
private let peerCountRange: ClosedRange<Int>?
private let didFinishSubject = CompletionSubject()
private let wasCancelledSubject = CompletionSubject()
}
}
// MARK: - internal
extension MCBrowserViewController.View {
var didFinishPublisher: AnyPublisher<Void, Never> { didFinishSubject.eraseToAnyPublisher() }
var wasCancelledPublisher: AnyPublisher<Void, Never> { wasCancelledSubject.eraseToAnyPublisher() }
}
// MARK: - private
private extension MCBrowserViewController {
typealias CompletionSubject = PassthroughSubject<Void, Never>
}
// MARK: - UIViewControllerRepresentable
extension MCBrowserViewController.View: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MCBrowserViewController {
let browser = MCBrowserViewController(
serviceType: serviceType,
session: session
)
browser.delegate = self
if let peerCountRange = peerCountRange {
browser.minimumNumberOfPeers = peerCountRange.lowerBound
browser.maximumNumberOfPeers = peerCountRange.upperBound
}
return browser
}
func updateUIViewController(_: MCBrowserViewController, context _: Context) { }
}
// MARK: - MCBrowserViewControllerDelegate
extension MCBrowserViewController.View: MCBrowserViewControllerDelegate {
func browserViewControllerDidFinish(_: MCBrowserViewController) {
didFinishSubject.send()
}
func browserViewControllerWasCancelled(_: MCBrowserViewController) {
wasCancelledSubject.send()
}
}
I don't have a full detailed answer for your question, but your solution have some problems.
In SwiftUI, if we update a View, it calls init to recreate the View, and then call updateUIViewController.
In your case, whenever you update your View, not only your view is recreated, your two subjects will be recreated too, so anything attaches to the Publisher after the recreation won't receive events any more.
maybe that's the reason we prefer to use Coordinator.

ReactiveSwift Simple Example

I've read the documentation, gone through their wonderful Playground example, searched S.O., and reached the extent of my google-fu, but I cannot for the life of me wrap my head around how to use ReactiveSwift.
Given the following....
class SomeModel {
var mapType: MKMapType = .standard
var selectedAnnotation: MKAnnotation?
var annotations = [MKAnnotation]()
var enableRouteButton = false
// The rest of the implementation...
}
class SomeViewController: UIViewController {
let model: SomeModel
let mapView = MKMapView(frame: .zero) // It's position is set elsewhere
#IBOutlet var routeButton: UIBarButtonItem?
init(model: SomeModel) {
self.model = model
super.init(nibName: nil, bundle: nil)
}
// The rest of the implementation...
}
....how can I use ReactiveSwift to initialize SomeViewController with the values from SomeModel, then update SomeViewController whenever the values in SomeModel change?
I've never used reactive anything before, but everything I read leads me to believe this should be possible. It is making me crazy.
I realize there is much more to ReactiveSwift than what I'm trying to achieve in this example, but if someone could please use it to help me get started, I would greatly appreciate it. I'm hoping once I get this part, the rest will just "click".
First you'll want to use MutableProperty instead of plain types in your Model. This way, you can observe changes to them.
class Model {
let mapType = MutableProperty<MKMapType>(.standard)
let selectedAnnotation = MutableProperty<MKAnnotation?>(nil)
let annotations = MutableProperty<[MKAnnotation]>([])
let enableRouteButton = MutableProperty<Bool>(false)
}
In your ViewController, you can then bind those and observe those however necessary:
class SomeViewController: UIViewController {
let viewModel: Model
let mapView = MKMapView(frame: .zero) // It's position is set elsewhere
#IBOutlet var routeButton: UIBarButtonItem!
init(viewModel: Model) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
routeButton.reactive.isEnabled <~ viewModel.enableRouteButton
viewModel.mapType.producer.startWithValues { [weak self] mapType in
// Process new map type
}
// Rest of bindings
}
// The rest of the implementation...
}
Note that MutableProperty has both, a .signal as well as a .signalProducer.
If you immediately need the current value of a MutableProperty (e.g. for initial setup), use .signalProducer which immediately sends an event with the current value as well as any changes.
If you only need to react to future changes, use .signal which will only send events for future changes.
Reactive Cocoa 5.0 will add UIKit bindings which you can use to directly bind UI elements to your reactive layer like done with routeButton in the example.

iOS8 TabbarController inside a UISplitviewController Master

I've tried to expand the default Apple MasterDetail Template by adding a UITabbarController in front of the UINavigationController of the MasterView, so there is a structure like this:
UISplitViewController (Master) > UITabbarController > UINavigationController > UITableViewController
But if I run the App, after changing application(didFinishLaunchingWithOptions) to use the correct ViewController, and try to perform the ShowDetails Segue the DetailsView ist presented Modally on the iPhone. On the other side the iPad Version is working as expected.
What am I forgot to do? Or how can I fix it?
I figured out how to put the detail on to the master's UINavigationController instead of presenting it modally over the UITabBarController.
Using the UISplitViewControllerDelegate method
- splitViewController:showDetailViewController:sender:
In case the UISplitViewController is collapsed get the masters navigation controller and push the detail view onto this navigation controller:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController
showDetailViewController:(UIViewController *)vc
sender:(id)sender {
NSLog(#"UISplitViewController collapsed: %d", splitViewController.collapsed);
// TODO: add introspection
if (splitViewController.collapsed) {
UITabBarController *master = (UITabBarController *) splitViewController.viewControllers[0];
UINavigationController *masterNavigationController = (UINavigationController *)master.selectedViewController;
// push detail view on the navigation controller
[masterNavigationController pushViewController:vc animated:YES];
return YES;
}
return NO;
}
Just to update the answers above. Since you can't push navigation controllers anymore, you have to push its top view controller instead.
func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
if splitViewController.collapsed {
let tabBarController = splitViewController.viewControllers.first as! UITabBarController
let selectedNavigationViewController = tabBarController.selectedViewController as! UINavigationController
// Push view controller
var viewControllerToPush = vc
if let navController = vc as? UINavigationController {
viewControllerToPush = navController.topViewController
}
selectedNavigationViewController.pushViewController(viewControllerToPush, animated: true)
return true
}
return false
}
Here's my solution. Place in MasterViewController.m and remember to give your detail view a Storyboard ID in IB. In my case 'detail'.
-(BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"showDetail"] && self.splitViewController.collapsed) {
DetailViewController *myController = (DetailViewController *)[self.storyboard instantiateViewControllerWithIdentifier:#"detail"];
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
NSManagedObject *object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
[myController setDetailItem:object];
[self.navigationController showViewController:myController sender:self];
return NO;
}
return YES;
}
There is another way to do it without code.
After you embedded the the UINavigationController in the TabBarController embed the TabBarController in another UINavigationController. So you will have: SplitViewController -> Master -> NavCon -> TabBar -> NavCon -> TableViewController.
It's much easier doing like this, but there a bug that I haven't found out how to fix. The navigation bar presented will be that of the TabBarController, not the TableViewController. Any ideas how to fix that?
Subclass TabBarController like this:
- (void)showViewController:(UIViewController *)vc sender:(id)sender
{
if ([self.selectedViewController isKindOfClass:UINavigationController.class])
[self.selectedViewController showViewController:vc sender:sender];
else
[super showViewController:vc sender:sender];
}
- (UIViewController*)separateSecondaryViewControllerForSplitViewController:(UISplitViewController *)splitViewController
{
return [self.selectedViewController separateSecondaryViewControllerForSplitViewController:splitViewController];
}
- (void)collapseSecondaryViewController:(UIViewController *)secondaryViewController forSplitViewController:(UISplitViewController *)splitViewController
{
[self.selectedViewController.navigationController collapseSecondaryViewController:secondaryViewController forSplitViewController:splitViewController];
}
See this question for complete explanation.
Here is an alternative that is based on testing the size classes of the splitViewController :
Use a custom UISplitViewController (subclass)
Override the showDetailViewController operation
Use the traitCollection to determine the class of the UISplitViewController
If the horizontal class is Compact, get the navigationController to call showViewController
Here is the the code of the custom UISplitViewController :
import UIKit
class CustomSplitViewController: UISplitViewController {
override func showDetailViewController(vc: UIViewController!, sender: AnyObject!) {
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact) {
if let tabBarController = self.viewControllers[0] as? UITabBarController {
if let navigationController = tabBarController.selectedViewController as? UINavigationController {
navigationController.showViewController(vc, sender: sender)
return
}
}
}
super.showDetailViewController(vc, sender: sender)
}
}
Do not forget to the set the custom class in the storyboard.
Tested in the simulator of iPhone 6, iPhone 6+ and iPad Air and worked as expected.