I have an master detail app in iPad, and when run the app in portrait mode the sidebar is hidden. I need to push Back button to open the sidebar.
Can anyone help me to show the sidebar by default?
I found an answer that suggest to use StackNavigationViewStyle when the app is in portrait, but then the app seems like a giant iPhone and dissapears the master class like a sidebar to appear like a view.
Thats my code.
struct ContentView: View {
var body: some View {
NavigationView {
MyMasterView()
DetailsView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct MyMasterView: View {
var people = ["Option 1", "Option 2", "Option 3"]
var body: some View {
List {
ForEach(people, id: \.self) { person in
NavigationLink(destination: DetailsView()) {
Text(person)
}
}
}
}
}
struct DetailsView: View {
var body: some View {
Text("Hello world")
.font(.largeTitle)
}
}
Thank you
It can be done, but for now it requires access to UIKit's UISplitViewController via UIViewRepresentable. Here's an example, based on a solution described here.
import SwiftUI
import UIKit
struct UIKitShowSidebar: UIViewRepresentable {
let showSidebar: Bool
func makeUIView(context: Context) -> some UIView {
let uiView = UIView()
if self.showSidebar {
DispatchQueue.main.async { [weak uiView] in
uiView?.next(of: UISplitViewController.self)?
.show(.primary)
}
} else {
DispatchQueue.main.async { [weak uiView] in
uiView?.next(of: UISplitViewController.self)?
.show(.secondary)
}
}
return uiView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
DispatchQueue.main.async { [weak uiView] in
uiView?.next(
of: UISplitViewController.self)?
.show(showSidebar ? .primary : .secondary)
}
}
}
extension UIResponder {
func next<T>(of type: T.Type) -> T? {
guard let nextValue = self.next else {
return nil
}
guard let result = nextValue as? T else {
return nextValue.next(of: type.self)
}
return result
}
}
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink("Primary view (a.k.a. Sidebar)", destination: DetailView())
}
NothingView()
}
}
}
struct DetailView: View {
var body: some View {
Text("Secondary view (a.k.a Detail)")
}
}
struct NothingView: View {
#State var showSidebar: Bool = false
var body: some View {
Text("Nothing to see")
if UIDevice.current.userInterfaceIdiom == .pad {
UIKitShowSidebar(showSidebar: showSidebar)
.frame(width: 0,height: 0)
.onAppear {
showSidebar = true
}
.onDisappear {
showSidebar = false
}
}
}
}
In iOS 16 you'll be able to control it using columnVisibility
Using Swift5.3.2, iOS14.4.1, Xcode12.4,
I am trying to make two simultaneous gestures work in SwiftUI.
The child-View at question is a VideoPlayer.
The parent-View is a ZStack.
I am using the .simultaneousGesture modifier, hoping that this will allow all child-View gestures to still go through.
But they don't !
The following code works but does not allow the VideoPlayer's control gestures to be recognised.
In fact, the restartScannerTapGesture TapGesture in my example does always kick in - and the VideoPlayer control gestures (like pause, play, stop etc.) are unfortunately ignored.
Any idea on how to get child-View gestures work when added a gesture to a parent-View in Swift UI ??
struct ParentView: View {
#EnvironmentObject var myURLList
var body: some View {
let restartScannerTapGesture = TapGesture(count: 1)
.onEnded {
actionClickID = UUID()
}
ZStack {
if let url = URL(fileURLWithPath: myURLList[0].path){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
.onAppear() {
isVideo = false
}
} else if url.containsVideo {
CustomPlayerView(url: url, isVideo: $isVideo)
.onAppear() {
isVideo = true
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
}
}
.simultaneousGesture(restartScannerTapGesture)
}
And here is the CustomPlayerView that shows the Video controls in the first place.
It clearly is the child-View at question. Its gestures should also work, but they don't. Why ??
import SwiftUI
import AVKit
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer?
func loadFromUrl(url: URL) {
avPlayer = AVPlayer(url: url)
}
func playVideo() {
avPlayer?.play()
}
func stopVideo() {
avPlayer?.pause()
avPlayer?.replaceCurrentItem(with: nil)
}
}
struct CustomPlayerView: View {
var url : URL
#Binding var isVideo: Bool
#StateObject private var playerViewModel = PlayerViewModel()
var body: some View {
ZStack {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
.onAppear() {
isVideo = false
}
} else if url.containsVideo {
CustomPlayerView(url: url, isVideo: $isVideo)
.onAppear() {
isVideo = true
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
}
}
And the needed extension:
extension URL {
func mimeType() -> String {
let pathExtension = self.pathExtension
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() {
if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimetype as String
}
}
return "application/octet-stream"
}
var containsImage: Bool {
let mimeType = self.mimeType()
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() else {
return false
}
return UTTypeConformsTo(uti, kUTTypeImage)
}
var containsAudio: Bool {
let mimeType = self.mimeType()
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() else {
return false
}
return UTTypeConformsTo(uti, kUTTypeAudio)
}
var containsVideo: Bool {
let mimeType = self.mimeType()
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() else {
return false
}
return UTTypeConformsTo(uti, kUTTypeMovie)
}
}
How to detect device rotation in SwiftUI and re-draw view components?
I have a #State variable initialized to the value of UIScreen.main.bounds.width when the first appears. But this value doesn't change when the device orientation changes. I need to redraw all components when the user changes the device orientation.
Here‘s an idiomatic SwiftUI implementation based on a notification publisher:
struct ContentView: View {
#State var orientation = UIDevice.current.orientation
let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
.makeConnectable()
.autoconnect()
var body: some View {
Group {
if orientation.isLandscape {
Text("LANDSCAPE")
} else {
Text("PORTRAIT")
}
}.onReceive(orientationChanged) { _ in
self.orientation = UIDevice.current.orientation
}
}
}
The output of the publisher (not used above, therefor _ as the block parameter) also contains the key "UIDeviceOrientationRotateAnimatedUserInfoKey" in its userInfo property if you need to know if the rotation should be animated.
#dfd provided two good options, I am adding a third one, which is the one I use.
In my case I subclass UIHostingController, and in function viewWillTransition, I post a custom notification.
Then, in my environment model I listen for such notification which can be then used in any view.
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
Group {
if model.landscape {
Text("LANDSCAPE")
} else {
Text("PORTRAIT")
}
}
}
}
In SceneDelegate.swift:
window.rootViewController = MyUIHostingController(rootView: ContentView().environmentObject(Model(isLandscape: windowScene.interfaceOrientation.isLandscape)))
My UIHostingController subclass:
extension Notification.Name {
static let my_onViewWillTransition = Notification.Name("MainUIHostingController_viewWillTransition")
}
class MyUIHostingController<Content> : UIHostingController<Content> where Content : View {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
NotificationCenter.default.post(name: .my_onViewWillTransition, object: nil, userInfo: ["size": size])
super.viewWillTransition(to: size, with: coordinator)
}
}
And my model:
class Model: ObservableObject {
#Published var landscape: Bool = false
init(isLandscape: Bool) {
self.landscape = isLandscape // Initial value
NotificationCenter.default.addObserver(self, selector: #selector(onViewWillTransition(notification:)), name: .my_onViewWillTransition, object: nil)
}
#objc func onViewWillTransition(notification: Notification) {
guard let size = notification.userInfo?["size"] as? CGSize else { return }
landscape = size.width > size.height
}
}
There is an easier solution that the one provided by #kontiki, with no need for notifications or integration with UIKit.
In SceneDelegate.swift:
func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
model.environment.toggle()
}
In Model.swift:
final class Model: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var environment: Bool = false { willSet { objectWillChange.send() } }
}
The net effect is that the views that depend on the #EnvironmentObject model will be redrawn each time the environment changes, be it rotation, changes in size, etc.
SwiftUI 2
Here is a solution that is not using the SceneDelegate (which is missing in the new SwiftUI life cycle).
It also uses interfaceOrientation from the current window scene instead of the
UIDevice.current.orientation (which is not set when the app starts).
Here is a demo:
struct ContentView: View {
#State private var isPortrait = false
var body: some View {
Text("isPortrait: \(String(isPortrait))")
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
guard let scene = UIApplication.shared.windows.first?.windowScene else { return }
self.isPortrait = scene.interfaceOrientation.isPortrait
}
}
}
It is also possible to use an extension for accessing the current window scene:
extension UIApplication {
var currentScene: UIWindowScene? {
connectedScenes
.first { $0.activationState == .foregroundActive } as? UIWindowScene
}
}
and use it like this:
guard let scene = UIApplication.shared.currentScene else { return }
If someone is also interested in the initial device orientation. I did it as follows:
Device.swift
import Combine
final class Device: ObservableObject {
#Published var isLandscape: Bool = false
}
SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
// created instance
let device = Device() // changed here
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// ...
// added the instance as environment object here
let contentView = ContentView().environment(\.managedObjectContext, context).environmentObject(device)
if let windowScene = scene as? UIWindowScene {
// read the initial device orientation here
device.isLandscape = (windowScene.interfaceOrientation.isLandscape == true)
// ...
}
}
// added this function to register when the device is rotated
func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
device.isLandscape.toggle()
}
// ...
}
I think easy repainting is possible with addition of
#Environment(\.verticalSizeClass) var sizeClass
to View struct.
I have such example:
struct MainView: View {
#EnvironmentObject var model: HamburgerMenuModel
#Environment(\.verticalSizeClass) var sizeClass
var body: some View {
let tabBarHeight = UITabBarController().tabBar.frame.height
return ZStack {
HamburgerTabView()
HamburgerExtraView()
.padding(.bottom, tabBarHeight)
}
}
}
As you can see I need to recalculate tabBarHeight to apply correct bottom padding on Extra View, and addition of this property seems to correctly trigger repainting.
With just one line of code!
I tried some of the previous answers, but had a few problems. One of the solutions would work 95% of the time but would screw up the layout every now and again. Other solutions didn't seem to be in tune with SwiftUI's way of doing things. So I came up with my own solution. You might notice that it combines features of several previous suggestions.
// Device.swift
import Combine
import UIKit
final public class Device: ObservableObject {
#Published public var isLandscape: Bool = false
public init() {}
}
// SceneDelegate.swift
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var device = Device()
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
.environmentObject(device)
if let windowScene = scene as? UIWindowScene {
// standard template generated code
// Yada Yada Yada
let size = windowScene.screen.bounds.size
device.isLandscape = size.width > size.height
}
}
// more standard template generated code
// Yada Yada Yada
func windowScene(_ windowScene: UIWindowScene,
didUpdate previousCoordinateSpace: UICoordinateSpace,
interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation,
traitCollection previousTraitCollection: UITraitCollection) {
let size = windowScene.screen.bounds.size
device.isLandscape = size.width > size.height
}
// the rest of the file
// ContentView.swift
import SwiftUI
struct ContentView: View {
#EnvironmentObject var device : Device
var body: some View {
VStack {
if self.device.isLandscape {
// Do something
} else {
// Do something else
}
}
}
}
Inspired by #caram solution, I grab the isLandscape property from windowScene
In SceneDelegate.swift, get the current orientation from window.windowScene.interfaceOrientation
...
var model = Model()
...
func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
model.isLandScape = windowScene.interfaceOrientation.isLandscape
}
In this way, we'll get true from the start if the user launches the app from the landscape mode.
Here is the Model
class Model: ObservableObject {
#Published var isLandScape: Bool = false
}
And we can use it in the exact same way as #kontiki suggested
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
Group {
if model.isLandscape {
Text("LANDSCAPE")
} else {
Text("PORTRAIT")
}
}
}
}
Here is an abstraction that allows you to wrap any part of your view tree in optional orientation based behavior, as a bonus, it doesn't rely on UIDevice orientation but instead bases it on the geometry of the space, this allows it to work in swift preview, as well as provide logic for different layouts based specifically on the container for your view:
struct OrientationView<L: View, P: View> : View {
let landscape : L
let portrait : P
var body: some View {
GeometryReader { geometry in
Group {
if geometry.size.width > geometry.size.height { self.landscape }
else { self.portrait }
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
init(landscape: L, portrait: P) {
self.landscape = landscape
self.portrait = portrait
}
}
struct OrientationView_Previews: PreviewProvider {
static var previews: some View {
OrientationView(landscape: Text("Landscape"), portrait: Text("Portrait"))
.frame(width: 700, height: 600)
.background(Color.gray)
}
}
Usage: OrientationView(landscape: Text("Landscape"), portrait: Text("Portrait"))
It's easy to go without notifications, delegation methods, events, changes to SceneDelegate.swift, window.windowScene.interfaceOrientation and so on.
try running this in simulator and rotating device.
struct ContentView: View {
let cards = ["a", "b", "c", "d", "e"]
#Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
let arrOfTexts = {
ForEach(cards.indices) { (i) in
Text(self.cards[i])
}
}()
if (horizontalSizeClass == .compact) {
return VStack {
arrOfTexts
}.erase()
} else {
return VStack {
HStack {
arrOfTexts
}
}.erase()
}
}
}
extension View {
func erase() -> AnyView {
return AnyView(self)
}
}
The best way to do this in iOS 14:
// GlobalStates.swift
import Foundation
import SwiftUI
class GlobalStates: ObservableObject {
#Published var isLandScape: Bool = false
}
// YourAppNameApp.swift
import SwiftUI
#main
struct YourAppNameApp: App {
// GlobalStates() is an ObservableObject class
var globalStates = GlobalStates()
// Device Orientation
let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
.makeConnectable()
.autoconnect()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(globalStates)
.onReceive(orientationChanged) { _ in
// Set the state for current device rotation
if UIDevice.current.orientation.isFlat {
// ignore orientation change
} else {
globalStates.isLandscape = UIDevice.current.orientation.isLandscape
}
}
}
}
// Now globalStates.isLandscape can be used in any view
// ContentView.swift
import SwiftUI
struct ContentView: View {
#EnvironmentObject var globalStates: GlobalStates
var body: some View {
VStack {
if globalStates.isLandscape {
// Do something
} else {
// Do something else
}
}
}
}
I wanted to know if there is simple solution within SwiftUI that works with any enclosed view so it can determine a different landscape/portrait layout. As briefly mentioned by #dfd GeometryReader can be used to trigger an update.
Note that this works in the special occasions where use of the standard size class/traits do not provide sufficient information to implement a design. For example, where a different layout is required for portrait and landscape but where both orientations result in a standard size class being returned from the environment. This happens with the largest devices, like the max sized phones and with iPads.
This is the 'naive' version and this does not work.
struct RotatingWrapper: View {
var body: some View {
GeometryReader { geometry in
if geometry.size.width > geometry.size.height {
LandscapeView()
}
else {
PortraitView()
}
}
}
}
This following version is a variation on a rotatable class that is a good example of function builders from #reuschj but just simplified for my application requirements https://github.com/reuschj/RotatableStack/blob/master/Sources/RotatableStack/RotatableStack.swift
This does work
struct RotatingWrapper: View {
func getIsLandscape(geometry:GeometryProxy) -> Bool {
return geometry.size.width > geometry.size.height
}
var body: some View {
GeometryReader { geometry in
if self.getIsLandscape(geometry:geometry) {
Text("Landscape")
}
else {
Text("Portrait").rotationEffect(Angle(degrees:90))
}
}
}
}
That is interesting because I'm assuming that some SwiftUI magic has caused this apparently simple semantic change to activate the view re-rendering.
One more weird trick that you can use this for, is to 'hack' a re-render this way, throw away the result of using the GeometryProxy and perform a Device orientation lookup. This then enables use of the full range of orientations, in this example the detail is ignored and the result used to trigger a simple portrait and landscape selection or whatever else is required.
enum Orientation {
case landscape
case portrait
}
struct RotatingWrapper: View {
func getOrientation(geometry:GeometryProxy) -> Orientation {
let _ = geometry.size.width > geometry.size.height
if UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft || UIDevice.current.orientation == UIDeviceOrientation.landscapeRight {
return .landscape
}
else {
return .portrait
}
}
var body: some View {
ZStack {
GeometryReader { geometry in
if self.getOrientation(geometry: geometry) == .landscape {
LandscapeView()
}
else {
PortraitView()
}
}
}
}
}
Furthermore, once your top level view is being refreshed you can then use DeviceOrientation directly, such as the following in child views as all child views will be checked once the top level view is 'invalidated'
Eg: In the LandscapeView() we can format child views appropriately for its horizontal position.
struct LandscapeView: View {
var body: some View {
HStack {
Group {
if UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft {
VerticallyCenteredContentView()
}
Image("rubric")
.resizable()
.frame(width:18, height:89)
//.border(Color.yellow)
.padding([UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft ? .trailing : .leading], 16)
}
if UIDevice.current.orientation == UIDeviceOrientation.landscapeRight {
VerticallyCenteredContentView()
}
}.border(Color.pink)
}
}
This seems to work for me. Then just init and use Orientation instance as environmentobject
class Orientation: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var isLandScape:Bool = false {
willSet {
objectWillChange.send() }
}
var cancellable: Cancellable?
init() {
cancellable = NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification)
.map() { _ in (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight)}
.removeDuplicates()
.assign(to: \.isLandScape, on: self)
}
}
I got
"Fatal error: No ObservableObject of type SomeType found"
because I forgot to call contentView.environmentObject(orientationInfo) in SceneDelegate.swift. Here is my working version:
// OrientationInfo.swift
final class OrientationInfo: ObservableObject {
#Published var isLandscape = false
}
// SceneDelegate.swift
var orientationInfo = OrientationInfo()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// ...
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(orientationInfo))
// ...
}
func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
orientationInfo.isLandscape = windowScene.interfaceOrientation.isLandscape
}
// YourView.swift
#EnvironmentObject var orientationInfo: OrientationInfo
var body: some View {
Group {
if orientationInfo.isLandscape {
Text("LANDSCAPE")
} else {
Text("PORTRAIT")
}
}
}
Try to use horizontalSizeClass & verticalSizeClass:
import SwiftUI
struct DemoView: View {
#Environment(\.horizontalSizeClass) var hSizeClass
#Environment(\.verticalSizeClass) var vSizeClass
var body: some View {
VStack {
if hSizeClass == .compact && vSizeClass == .regular {
VStack {
Text("Vertical View")
}
} else {
HStack {
Text("Horizontal View")
}
}
}
}
}
Found it in this tutorial. Related Apple's documentation.
Another hack to detect the change of orientation but also the splitView. (inspired by #Rocket Garden)
import SwiftUI
import Foundation
struct TopView: View {
var body: some View {
GeometryReader{
geo in
VStack{
if keepSize(geo: geo) {
ChildView()
}
}.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
}.background(Color.red)
}
func keepSize(geo:GeometryProxy) -> Bool {
MyScreen.shared.width = geo.size.width
MyScreen.shared.height = geo.size.height
return true
}
}
class MyScreen:ObservableObject {
static var shared:MyScreen = MyScreen()
#Published var width:CGFloat = 0
#Published var height:CGFloat = 0
}
struct ChildView: View {
// The presence of this line also allows direct access to up-to-date UIScreen.main.bounds.size.width & .height
#StateObject var myScreen:MyScreen = MyScreen.shared
var body: some View {
VStack{
if myScreen.width > myScreen.height {
Text("Paysage")
} else {
Text("Portrait")
}
}
}
}
I have updated https://stackoverflow.com/a/62370919/7139611 to load it for the initial view and make it as work globally using Environment object.
import SwiftUI
class Orientation: ObservableObject {
#Published var isLandscape: Bool = UIDevice.current.orientation.isLandscape
}
struct ContentView: View {
#StateObject var orientation = Orientation()
#State var initialOrientationIsLandScape = false
let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
.makeConnectable()
.autoconnect()
var body: some View {
Group {
if orientation.isLandscape {
Text("LANDSCAPE")
} else {
Text("PORTRAIT")
}
}
.onReceive(orientationChanged, perform: { _ in
if initialOrientationIsLandScape {
initialOrientationIsLandScape = false
} else {
orientation.isLandscape = UIDevice.current.orientation.isLandscape
}
})
.onAppear {
orientation.isLandscape = UIDevice.current.orientation.isLandscape
initialOrientationIsLandScape = orientation.isLandscape
}
}
}
For those wishing to manipulate some other variables/state on device rotation change, here's a solution:
struct ContentView: View {
#Environment(\.verticalSizeClass) private var verticalSizeClass
var body: some View {
VStack {
...
}
.onChange(of: verticalSizeClass, perform: { newValue in
// Update your variables/state here
}
}
}
Its important to use verticalSizeClass instead of horizontalSizeClass because the former changes when iPhone orientation is changed, but for some iPhone models the latter won't change on device rotation.
This also won't work on iPad/macOS - you'll need to use a combo of both horizontal and vertical size classes to detect rotation on those. You can see the various configurations and what values the size classes will report here under the Device size classes subheading: https://developer.apple.com/design/human-interface-guidelines/foundations/layout
In my an viewController I have this function
...{
let vc = UIHostingController(rootView: SwiftUIView())
present(vc, animated: true, completion: nil)
}
Which present the following SwiftUIView.
Q How to dismiss the SwiftUIView when CustomButton pressed?
struct SwiftUIView : View {
var body: some View {
CustomButton()
}
}
struct CustomButton: View {
var body: some View {
Button(action: {
self.buttonAction()
}) {
Text(buttonTitle)
}
}
func buttonAction() {
//dismiss the SwiftUIView when this button pressed
}
}
struct CustomButton: View {
var body: some View {
Button(action: {
self.buttonAction()
}) {
Text(buttonTitle)
}
}
func buttonAction() {
if let topController = UIApplication.topViewController() {
topController.dismiss(animated: true)
}
}
}
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
Or if this doesn't work because you have a different view controller on top or if you need to use view life cycle events (onDisappear and onAppear won't work with UIHostingController).
You can use instead:
final class SwiftUIViewController: UIHostingController<CustomButton> {
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: CustomButton())
rootView.dismiss = dismiss
}
init() {
super.init(rootView: CustomButton())
rootView.dismiss = dismiss
}
func dismiss() {
dismiss(animated: true, completion: nil)
}
override func viewWillDisappear(_ animated: Bool) {
rootView.prepareExit()
}
override func viewDidDisappear(_ animated: Bool) {
rootView.doExit()
}
}
struct CustomButton: View {
var dismiss: (() -> Void)?
var body: some View {
Button(action: dismiss! ) {
Text("Dismiss")
}
}
func prepareExit() {
// code to execute on viewWillDisappear
}
func doExit() {
// code to execute on viewDidDisappear
}
}
I am working on an iOS application, in which all ViewControllers support only Portrait orientation, except one ViewController, which supports both Portrait and landscape. I try this code for that ViewController
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscapeLeft
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .landscapeLeft
}
but this didn't work, Can any one help ?
thanks
in AppDelegate add this event
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let navigationController = UIApplication.topViewController() {
if navigationController is YOURVIEWCONTROLLERNAME {
return UIInterfaceOrientationMask.all
}
else {
return UIInterfaceOrientationMask.portrait
}
}
return UIInterfaceOrientationMask.portrait
}
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
change YOURVIEWCONTROLLERNAME to your view controller