I'm doing radio player in my app. And want to have widget with info + control buttons, that will be persisted over all controllers in app while playing. Like in itunes or like google chromecast container that will push from bottom all the elements of other viewcontroller (not overlay elements)
I know that overlay view can be added in appdelegate to keywindow, as GoogleCast Container added:
let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
let castContainerVC: GCKUICastContainerViewController = GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
castContainerVC.miniMediaControlsItemEnabled = true
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = castContainerVC
self.window?.makeKeyAndVisible()
But I can't understand, how I can design and instantiate my controller + add it to window + hide when nothing is playing.
I forgot to mention, that app is already up and running. There are about 30 view controllers inside navigation bar
I would create a window on top of the current window to which I would put that radio view. Then the radio view would be always on top of the view hierarchy in the standard window. To allow touch events be processed by the standard window that is under the radio window, you would need to override hitTest in the radio window to make sure it wont process events that are not supposed to be processed by it.
You can take this as an example:
import UIKit
class RadioController: UIViewController {
fileprivate struct Static {
static let window: UIHigherWindow = {
let window = UIHigherWindow(frame: UIScreen.main.bounds)
window.rootViewController = RadioController()
window.windowLevel = UIWindowLevelAlert - 1
return window
}()
}
override func loadView() {
// use passView, as it will pass the touch events tu underlying window
self.view = PassView()
self.view.backgroundColor = UIColor.clear
}
override func viewDidLoad() {
super.viewDidLoad()
// configure whatever subview you want, in your case the radio view
let radioView = UIView(frame: CGRect(x: 0, y: UIScreen.main.bounds.height - 50, width: UIScreen.main.bounds.width, height: 50))
radioView.backgroundColor = .red
self.view.addSubview(radioView)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
// this will now define the status bar style, as it will become the topmost window for most of the time
return .default
}
static var showing: Bool {
get {
return !Static.window.isHidden
}
}
static func present() {
Static.window.isHidden = false
}
static func hide() {
Static.window.isHidden = true
}
}
// MARK: Passing touch events to the back view
class PassView: UIView {}
class UIHigherWindow: UIWindow {
// this will allow touch events to be processed by the default window
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView!.isKind(of: PassView.self) {
return nil
}
return hitView
}
}
Solution
Embed your initial view controller inside a container view controller
Add your subviews to the bottom of the view for the container vc
Align the subviews so that they are offscreen below the main view
Create a method that when called animates the subview up from the
bottom to the appropriate position and another to animate the
subview back below the bottom
Place calls to the animation methods where appropriate in your code
Notes
You will want to add a custom class to your container vc so you can
call the animation methods on it
Related
I created an additional UIWindow that will get presented on top of the main window. The user presses a button and it transitions over the main window. The user can minimize the additional window and it will sit above the tabBar like it the picture below. They can enlarge it to cover the main window or dismiss it and it gets destroyed. It works fine.
I also created a custom action sheet that launches from the bottom of the screen. If the additional window completely covers the main window and the action sheet is launched, it will launch perfectly inside the additional window. If the additional window isn't on the screen it launches perfectly inside the main window.
The problem is if the additional window is minimized on screen and I want to launch the action sheet from the main window, the action sheet launches inside the additional window instead, it cannot distinguish between the two. I opened up the 3D visualizer and it showed that the main window was off and the additional window was on.
How can I distinguish between both windows when displaying the custom action sheet?
Btw if both windows are present and the action sheet is launched from the main window I hide the additional window. I also looked at other answers and they said to use UIApplication.shared.keyWindow.addSubview which I'm already doing.
CustomActionSheet Class:
var collectionView: UICollectionView!
var deltaY: CGFloat!
let height: CGFloat = 200 // 4 cells x 50 pts each
func displayActionSheet(){
if let window = UIApplication.shared.keyWindow {
// collectionView initialized...
window.addSubview(collectionView)
deltaY = window.frame.height - height
collectionView.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: height)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: [.curveEaseOut], animations: {
self.collectionView.frame = CGRect(x: 0, y: deltaY, width: self.collectionView.frame.width, height: self.collectionView.frame.height)
}
}
}
MainView Class:
#IBAction func mainWindowActionSheetButton(_ sender: UIButton) {
let customActionSheet = CustomActionSheet()
customActionSheet.displayActionSheet()
}
AdditionalWindow Class:
let myVC = MyController()
var nav: UINavigationController?
var window: UIWindow!
var maximized = true
override init() {
super.init()
window = UIWindow()
window.backgroundColor = .clear
window.windowLevel = UIWindowLevelStatusBar
nav = UINavigationController(rootViewController: myVC)
window.rootViewController = nav
window?.isHidden = false
window?.makeKeyAndVisible()
}
func maximizeOrMinimizeWindow() {
if maximized {
// show this full screen
} else {
// show this window minimized like in the picture
}
}
AnotherController Class that has the button that also launches the action sheet:
#IBAction func additionalWindowActionSheetButton(_ sender: UIButton) {
let customActionSheet = CustomActionSheet()
customActionSheet.displayActionSheet()
}
The summarize the advice in comments.
The problem is that the action sheet is always shown from the key window but the additional window remains key window even when minimized.
The obvious solution is to make the main window the key window when the additional one is being minimized. See UIWindow.makeKey() or UIWindow.makeKeyAndVisible().
Since UIApplication.shared.windows are ordered by window level (the back one first), you can always reach the main window using UIApplication.shared.windows.first.
Therefore
UIApplication.shared.windows.first?.makeKey()
will make the main window the key window and the minimized window will stop being the key window.
Here's the code breakdown to #Sulthan 's accepted answer. Read the comments in the code for an explanation.
let myVC = MyController()
var nav: UINavigationController?
var window: UIWindow!
var maximized = true
override init() {
super.init()
window = UIWindow()
window.backgroundColor = .clear
window.windowLevel = UIWindowLevelStatusBar
nav = UINavigationController(rootViewController: myVC)
window.rootViewController = nav!
window?.isHidden = false
// window?.makeKeyAndVisible() // don't call this because it doesn't need to be the keyWindow as of yet. The window?.isHidden property above this makes the window visible
}
func maximizeOrMinimizeWindow() {
if maximized {
window.first?.makeKey // when the additional window is maximized make it the keyWindow
} else {
UIApplication.shared.windows.first?.makeKey() // when the additional window is minimized set the main window back as the key window
}
}
It should also be stated then when the additional window is removed from the superview or destroyed make sure to set the main window back as the keyWindow using UIApplication.shared.windows.first?.makeKey()
I have a view controller that contains a collectionView with 2 sections. The header of the second section is a sticky header and it has a segmentedControl inside of it:
ParentViewController
--collectionView
--sectionOne // because there is specific data in sectionOne I cannot use a PageViewController
--sectionTwo
sectionTwoHeader // sticky header
[RedVC, BlueVC, GreenVC] // these should be the size of sectionTwo
When a segment is selected I'm using a ContainerVC that will show a view controller corresponding to each segment:
// each of of these color vcs have collectionViews inside of them
RedCollectionViewController(), BlueCollectionViewController(), GreenCollectionViewController()
The problem is when the segment is selected the collectionView isn't showing any of the color view controllers it's supposed to show. How do I add each color vc using addChildViewController() to a collectionView?
The collectionView w/ segmentedControl's selectedIndex:
class ParentViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
var collectionView: UICollectionView!
var containerController: ContainerController!
var vc: UIViewController!
override func viewDidLoad() {
super.viewDidLoad()
containerController = ContainerController()
}
#objc func selectedIndex(_ sender: UISegmentedControl){
let index = sender.selectedSegmentIndex
switch index {
case 0:
containerController.vcIdentifierReceivedFromParent(segment: "BlueVC")
break
case 1:
containerController.vcIdentifierReceivedFromParent(segment: "RedVC")
break
case 2:
containerController.vcIdentifierReceivedFromParent(segment: "GreenVC")
break
default: break
}
/*
// because of the X and Y values this adds the containerVC over the collectionView instead of under the sectionTwo segmented Control header
vc = containerController
addChildViewController(vc)
vc.view.frame = CGRect(x: 0,y: 0, width: collectionView.frame.width,height: collectionView.frame.height)
view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
lastViewController = vc
*/
}
}
ContainerVC:
class ContainerController: UIViewController {
var vc: UIViewController!
var lastViewController: UIViewController!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
vcIdentifierReceivedFromParent(segment: "RedVC")
}
func vcIdentifierReceivedFromParent(segment: String){
switch segment {
case "RedVC":
let redVC = RedCollectionViewController()
addVcToContainer(destination: redVC)
break
case "BlueVC":
let blueVC = BlueCollectionViewController()
addVcToContainer(destination: blueVC)
break
case "GreenVC":
let greenVC = GreenCollectionViewController()
addVcToContainer(destination: greenVC)
break
default: break
}
}
func addVcToContainer(destination: UIViewController) {
//Avoids creation of a stack of view controllers
if lastViewController != nil{
lastViewController.view.removeFromSuperview()
}
self.vc = destination
addChildViewController(vc)
vc.view.frame = CGRect(x: 0,y: 0, width: view.frame.width,height: view.frame.height)
view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
lastViewController = vc
}
}
You are adding Red / Blue / Green VCs to Container View controller that is referenced from inside ParentViewController. But you are adding each of them inside ContainerVC topmost view, whose frame is probably never set, as far as I can see from your code.
It's probably CGRectZero.
Adding child VC views to this view will result in they are getting wrongly positioned, or not positioned at all. Because Container View controller is nowhere in the view controller hierarchy. You are effectively doing everything within ParentViewController's viewDidLoad(). Most probably, ContainerVC's viewDidLoad is not even called. Hence its view is never initialised properly.
You probably do not need ContainerVC at all. Try adding children to ParentViewController, and try adding them after viewDidLoad() call, i.e. in viewDidAppear(), viewDidLayoutSubviews() and upon switch segment selection.
I have an app with a tabbar and a navbar.
I have a BaseVC and a DetailVC. I'm pushing DetailVC from BaseVC. I want the tabbar to be under the pushed VC DetailVC. I'm using hidesBottomBarWhenPushed to achieve it. It works great, but for some reason while it's animating the push the tabbar is still visible and just when the animation ends the tabbar is hidden. I want it to be under the pushed VC in the animation too.
My code is:
self.hidesBottomBarWhenPushed = true
self.navigationController?.pushViewController(detailVC, animated: true)
self.hidesBottomBarWhenPushed = false
And the result (the bug) is this:
Anyone has an idea why the tabbar "jumps"? Thank you!
Having looked at the project in question I have found one way to make it work:
Remove the viewWillLayoutSubviews from the TabBarViewController so that it is not determining the height of the tab bar anymore and thus not stopping the animation working correctly.
Create a new swift file called MyTabBar (or whatever you want) and put this in it:
import UIKit
class MyTabBar: UITabBar {
var tabBarHeight: CGFloat = 100
override func sizeThatFits(_ size: CGSize) -> CGSize {
let superSize = super.sizeThatFits(size)
return CGSize(width: superSize.width, height: self.tabBarHeight)
}
}
Create a storyboard called TabBarStoryboard (or whatever). It's not going to be used for anything other then to hold a UITabBarController which you later create.
In the storyboard set the class type of the UITabBarController to your class of TabBarViewController so it gets the correct class when instantiated.
In the storyboard set the class type of the UITabBar that belongs to the UITabBarController to MyTabBar so that it too is the correct class when instantiated.
In your RootViewController replace this:
fileprivate let tabBarViewController = TabBarViewController()
with this:
fileprivate lazy var tabBarViewController: TabBarViewController = {
let storyboard = UIStoryboard(name: "TabBarStoryboard", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "MyTabBarController") as! TabBarViewController
}()
In your TabBarViewController add this to the end of the viewDidLoad to set the height of the tab bar:
if let tabBar = self.tabBar as? MyTabBar {
tabBar.tabBarHeight = self.tabBarHeight
}
Now if you get all that correct you should have a tab bar the size you want and the animation should work correctly because the height of tab bar is not longer controlled by the viewDidLayoutSubviews method.
I had to use a storyboard to hold the basic UITabBarController because I couldn't find a way to set the class of its UITabBar property otherwise (if anyone knows a way add a comment.
In case this is difficult to follow I have uploaded my version of your project to dropbox and this is the link: PlayWiz-NewVersion.zip. Be careful as it will unzip to the same directory structure so extract it to a different folder than the original otherwise you will lose the original.
That method appears to work correctly for me and I see no reason for there to be any problem but test it thoroughly first.
I have a simpler variation of the above example (cheers by the way)
I pasted everything in viewDidLoad, but you can write it prettier.
class TabBarController: UITabBarController {
override func viewDidLoad() {
// create the normal buttons (controllers)
let viewControllers = [UINavigationController(rootViewController: firstButton), UINavigationController(rootViewController: secontButton)]
self.viewControllers = viewControllers
// create the middle rounded button
self.tabBar.addSubview(addItemButton)
// setup constraints
addItemButton.widthAnchor.constraint(equalToConstant: 64).isActive = true
addItemButton.heightAnchor.constraint(equalToConstant: 64).isActive = true
tabBar.centerXAnchor.constraint(equalTo: self.addItemButton.centerXAnchor).isActive = true
tabBar.topAnchor.constraint(equalTo: self.addItemButton.centerYAnchor, constant: -8).isActive = true
}
extension UITabBar {
// fix clicking the (+) external to the tabbar bounds
override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if (!self.clipsToBounds && !self.isHidden && self.alpha > 0.0) {
let subviews = self.subviews.reversed()
for member in subviews {
let subPoint = member.convert(point, from: self)
if let result:UIView = member.hitTest(subPoint, with:event) {
return result;
}
}
}
return super.hitTest(point, with: event)
}
// this fixes the "jumping" tabBar when using the "hidesBottomBarWhenPushed = true"
override open func sizeThatFits(_ size: CGSize) -> CGSize {
let superSize = super.sizeThatFits(size)
return CGSize(width: superSize.width, height: 85)
}
}
Now, just call hidesBottomBarWhenPushed = true and push the desired view controller.
I'm looking for how to implement like WhatsApp cell swiping, I already have implemented the cell swiping animation using UIPanGestureRecognizer, the only left is performing the interactive animation -adding the new UIViewController to the window and showing it based on the gesture recognizer velocity and X-axis value-.
Some additional note to be accurate on what I want to achieve:
I have a UITableViewController, which has custom UITableViewCells in it. I want to be able to drag a cell from left to right to start the interactive animations. (Note: I already have implemented the cell swiping).
The new UIViewController will be pushed from left right.
While swiping the cell, the UITableViewController's view will be moving to the right, at that point, I want to show the pushing UIViewController beside it.
Here's a GIF for more details on what I need (The GIF is swiping the cell from right to left, I need the opposite):
I suggest using SWRevealViewController. It is very easy to set up using their guide and it looks absolutely great. I have found that it even works better when you pre-load the UIViewController that you use to be what is shown underneath.
It adds a great user experience for the functionality you are looking for.
It can also be user interactive if you wish to opt-in to that functionality. I have not used the interactive feature but it is very easy to get up and running with just a few lines of code:
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let mainVC = storyboard.instantiateInitialViewController()
let menuStoryboard = UIStoryboard(name: "Menu", bundle: sdkBundle)
let menuNav = menuStoryboard.instantiateInitialViewController() as! UINavigationController
let mainRevealVC = SWRevealViewController(rearViewController: menuNav, frontViewController: mainVC)
mainRevealVC?.modalTransitionStyle = .crossDissolve
present(mainRevealVC!, animated: true, completion: nil)
And to get the reveal UIViewController to get shown, you just call
// Every UIViewController will have a `self.revealViewController()` when you `import SWRevealViewController`
self.revealViewController().revealToggle(animated: true)
I agree with #DonMag, a iOS slide menu might be your best bet. Here is an example of a simple one: SimpleSideMenu
Does it necessarily have to be a new controller behind the table view? Let me try to explain my approach on the WhatsApp example. Let's assume that the app has ChatController that has the table view with the chat and a ChatDetailController that is revealed with the swipe.
When you select a conversation, instead of presenting a ChatController present instead a ChatParent, that automatically creates and adds two children. The ChatController and ChatDetailController. Next define a protocol called SwipeableCellDelegate with a function cellDidSwipe(toPosition position: CGPoint) and make the ChatParent conform to it. When the cell is swiped, the parent can make the decision whether should the chat be moved away and if so, then how much. It can then simply move the ChatController view directly through its .view property, revealing the second child, the ChatDetailController behind it.
There are two downsides to this compared to the gif you posted.
The navigation bar doesn't fade from chat to chat detail. I would, however, argue that it is better to update the navigation bar when the animation completes, at least I personally am not a fan of this fade through where you can see both sets of navigation items at times. I would think that if chat is on screen then chat items should be present and only when detail view fully appears should the items be updated.
Second thing is the animated keyboard dismissal. I have no idea how to change keyboard frame to make it disappear proportionally to how far the user scrolls, but perhaps it could be dismissed automatically as soon as a swipe is detected? This is standard practice among many apps so it should be a decent solution.
Best of luck!
There is a very simple yet Perfect for your situation Library called SWNavigationController which implements just like UINavigationController's interactivePopGestureRecognizer also interactivePushGestureRecognizer. In your case you don't want the push to be triggered from UIScreenEdgePangesturerecognizer so you're better off customizing the implementation rather than installing the pod which is what I did. Here you can find the full simple project that does just what you asked.
I've made few modifications to SWNavigationController to support replacing UIScreenEdgePangesturerecognizer with a UIPanGestureRecognizer
import UIKit
// First in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let firstVc = ViewController()
let initialViewController: SWNavigationController = SWNavigationController(rootViewController: firstVc)
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
return true
}
// Your chat viewController
class ViewController: UIViewController {
var backgroundColors: [IndexPath : UIColor] = [ : ]
var swNavigationController: SWNavigationController {
return navigationController as! SWNavigationController
}
/// The collectionView if you're not using UICollectionViewController
lazy var collectionView: UICollectionView = {
let cv: UICollectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: self.layout)
cv.backgroundColor = UIColor.white
cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
cv.dataSource = self
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "chat vc"
view.addSubview(collectionView)
let panGestureRecognizer: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGestureRecognizer.delegate = self
collectionView.addGestureRecognizer(panGestureRecognizer)
// Replace navigation controller's interactivePushGestureRecognizer with our own pan recognizer.
// SWNavigationController uses uiscreenedgerecognizer by default which we don't need in our case.
swNavigationController.interactivePushGestureRecognizer = panGestureRecognizer
}
func handlePan(_ recognizer: UIPanGestureRecognizer) {
guard fabs(recognizer.translation(in: collectionView).x) > fabs(recognizer.translation(in: collectionView).y) else {
return
}
// create the new view controller upon .began
if recognizer.state == .began {
// disable scrolling(optional)
collectionView.isScrollEnabled = false
// pan location
let location: CGPoint = recognizer.location(in: collectionView)
// get indexPath of cell where pan is taking place
if let panCellIndexPath: IndexPath = collectionView.indexPathForItem(at: location) {
// clear previously pushed viewControllers
swNavigationController.pushableViewControllers.removeAllObjects()
// create detail view controller for pan indexPath
let dvc = DetailViewController(indexPath: panCellIndexPath, backgroundColor: backgroundColors[panCellIndexPath]!)
swNavigationController.pushableViewControllers.add(dvc)
}
} else if recognizer.state != .changed {
collectionView.isScrollEnabled = true
}
// let navigation controller handle presenting
// (you can consume the initial pan translation on x axis to drag the cell to the left until a defined threshold and call handleRightSwipe: only after that)
swNavigationController.handleRightSwipe(recognizer)
}
}
// Cell detail view controller
class DetailViewController: UIViewController {
var indexPath: IndexPath
var backgroundColor: UIColor
init(indexPath: IndexPath, backgroundColor: UIColor) {
self.indexPath = indexPath
self.backgroundColor = backgroundColor
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "detail vc at: \(indexPath.row)"
view.backgroundColor = backgroundColor
}
}
In my application I have a uiview at the top of the screen. When I tap that UIView I have a second View Controller slides up. What I would like to accomplish is that when the second View Controller slides into the screen it stops right before it covers up the UIView
What I am trying to accomplish is have a view slide up that contains store information such as hours open, address, phone number etc. So I was thinking that I could just have another view controller that holds all of this information. Only part is I want to stop it sliding up so it is flush with the uiview bar on top.
//tap to bring up the second view controller
#IBAction func showInfoVC(sender: AnyObject) {
self.performSegueWithIdentifier("showSecondVC", sender: self)
}
It sounds like your goals are:
Have a base View
Slide a second View part way up on the first
Assuming this is the case, there are multiple ways you could accomplish this, but Apple would probably recommend View Controller Containment. To accomplish this, you will have:
A SlidingContainerViewController.
This is a custom container View Controller that will hold our other two View Controllers
Some background View Controller
Some foreground View Controller
Here is a basic implementation of a custom SlidingContainerViewController
// SlidingContainerViewController.swift
import UIKit
class SlidingContainerViewController: UIViewController {
//MARK: - Init
init(frontViewController: UIViewController, backViewController: UIViewController) {
super.init(nibName: nil, bundle: nil)
frontViewContoller = frontViewController
backViewContoller = backViewController
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Public
var frontViewContoller: UIViewController = UIViewController()
var backViewContoller: UIViewController = UIViewController()
var splitOriginY: CGFloat = 160.0
func toggleFrontView() {
if frontIsVisible {
UIView.animateWithDuration(0.4) {
self.frontViewContoller.view.frame.origin.y = self.view.frame.height
self.frontIsVisible = false
}
} else {
UIView.animateWithDuration(0.4) {
self.frontViewContoller.view.frame.origin.y = self.splitOriginY
self.frontIsVisible = true
}
}
}
//MARK: - ViewLifecycle
override func viewDidLoad() {
super.viewDidLoad()
addChild(backViewContoller)
addChild(frontViewContoller)
self.frontViewContoller.view.frame.origin.y = self.view.frame.height
}
//MARK: - Private
var frontIsVisible = false
private func addChild(viewController: UIViewController) {
addChildViewController(viewController)
view.addSubview(viewController.view)
viewController.view.frame = view.bounds
viewController.didMoveToParentViewController(self)
}
}
You can then put any custom View Controllers that you want into this container View Controller. The bottom View Controller just needs to call toggleFrontView() on the container View Controller whenever it wants the slide to occur.
Below I've added two sample View Controllers for demonstrations purposes.
You can view the whole project on github: SlidingVC
*Note: This solution is implemented programmatically without Interface Builder. I personally build all of my apps completely in code this way. If you desired to use Interface Builder, you could accomplish the same thing using a Storyboard and custom segues. Here is a related tutorial: A Beginner’s Guide to Animated Custom Segues in iOS 8