SwiftUI App resets SKScene after becoming active again - swiftui

Every time my app goes into the background and then becomes active again, SpriteView always resets and the SKScene goes back to the beginning of the level. How can I stop this or what am I doing wrong?
My Code:
struct GameContainer: View {
private var scene: SKScene {
let scene = Splash()
scene.size = Size.shared.setupSceneSize()
scene.scaleMode = .aspectFit
return scene
}
var body: some View {
SpriteView(scene: scene, options: .ignoresSiblingOrder)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
}
}
struct Game: View {
...
var body: some View {
NavigationView {
ZStack {
GameContainer()
....
Update:
struct GameContainer: View {
static var scene = Splash()
var body: some View {
SpriteView(scene: GameContainer.scene, options: .ignoresSiblingOrder)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
}
}
class Splash: SKScene {
override init() {
super.init(size: Size.shared.setupSceneSize())
scaleMode = .aspectFit
}
....
}

Try setting the size and scale mode of the scene inside Splash(), then make scene static. I think that should solve the problem of the SpriteView reloading all the time.
struct GameContainer: View {
static var scene = Splash()
var body: some View {
SpriteView(scene: GameContainer.scene, options: .ignoresSiblingOrder)
.ignoresSafeArea()
.statusBar(hidden: true)
}
}
struct Game: View {
...
var body: some View {
NavigationView {
ZStack {
GameContainer()
....

Related

How could be change children #State property value from parent view in SwiftUI?

Given this code:
import SwiftUI
struct ContentView: View {
#State private var someView = DefaultView()
var body: some View {
VStack { someView }
//.onAppear { someView.change() } //1. Color not changing from here, isDisplay remains false
}
}
struct DefaultView: View {
#State private var isDisplay = false
func change() {
isDisplay = true
print("isDisplay = \(self.isDisplay)")
}
var body: some View {
Text("Hello!")
.foregroundColor(isDisplay ? .red : .blue)
//.onAppear { change() } //2. Color and isDisplay property is changing from here well
}
}
I'd like to call change func from parent view (ContentView 1.). That runs because the print is visible on the debug area, but there is no any changes, the isDisplay property does not change from false to true.
Why does this work that way? How can I change the #state property from the parent class?
Edit:
It could work:
struct DefaultView: View {
public var isDisplay: Bool
init() {
isDisplay = false
}
mutating func change() {
isDisplay = true
print("isDisplay = \(self.isDisplay)")
}
var body: some View {
Text("Hello!")
.foregroundColor(isDisplay ? .red : .blue)
}
}
The way you showed your code in question is not the way SwiftUI works, you are thinking in UIKit and coding SwiftUI?
Once you fired a View there is no return to use the imbedded function or any thing! SwiftUI-View is more like a gun build you can not change the direction or other property of it, after render is done! in the other hand UIKit-View is more like Rocket, after firing you have control on it and you can change destination and more other things as well. So there is deference between them, make your build lighter as possible. Because your going firing lots and SwiftUI should handel them as much as easily as possible.
Here the right way:
struct ContentView: View {
#State private var isDisplay: Bool = Bool()
var body: some View {
DefaultView(isDisplay: isDisplay)
Button("update") { isDisplay.toggle() }.padding()
}
}
struct DefaultView: View {
let isDisplay: Bool
var body: some View {
Text("Hello!")
.foregroundColor(isDisplay ? .red : .blue)
}
}
I would use #Binding to change the value of the property in your DefaultView from a parent view. Here is a brief example.
struct ContentView: View {
#State private var changeColor = false
var body: some View {
VStack {
Button {
changeColor.toggle()
} label: {
Text("Toggle Button")
.padding()
.foregroundColor(.red)
}
DefaultView(isDisplay: $changeColor)
}
}
}
struct DefaultView: View {
#Binding var isDisplay: Bool
var body: some View {
Text("Hello")
.foregroundColor(isDisplay ? .red : .blue)
}
}

How to display another view from an existing view in SwiftUI

I have an existing view displayed. After displaying that view for 2 seconds, I want to navigate or display another view. The following code does not work.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
UIGraphicsBeginImageContext(self.view.frame.size)
UIImage(named: "ProfileSplashScreen")?.draw(in: self.view.bounds)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
self.view.backgroundColor = UIColor(patternImage: image)
DispatchQueue.main.asyncAfter(deadline: .now()+2.0) {
GameSelectorController()
}
//=====================================================
import SwiftUI
struct GameSelectorController: UIViewController {
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
}
}
struct GameSelectorController_Previews: PreviewProvider {
static var previews: some View {
GameSelectorController()
}
}
In pure SwiftUI you would do something like this:
struct ContentView: View {
#State private var showFirst = true
var body: some View {
Group {
if showFirst {
StartView()
} else {
SecondView()
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.showFirst = false
}
}
}
}
struct StartView: View {
var body: some View {
Text("First")
}
}
struct SecondView: View {
var body: some View {
Text("Second")
}
}

Cannot make my status bar color to match my UINavigationBar color in SiwftUI

My problem appears to be a simple one. But I just spent hours looking for a solution!
I just have to make the status bar's background color conform to my UINavigationBar background's color (red in this sample).
I wrote a minimal code sample to reproduce my problem and help you for giving me a solution...
TestApp.swift
import SwiftUI
#main
struct TestApp: App {
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: phase) { newPhase in
switch newPhase {
case .active:
UINavigationBar.appearance().backgroundColor = UIColor.red
break
default:
break
}
}
}
}
ContentView.swift
struct DetailView1: View {
var body: some View {
Text("Detail view 1")
}
}
struct ContentView: View {
#State private var isDisplayDetailView1 = true
var body: some View {
NavigationView {
List {
Section {
NavigationLink(destination: DetailView1(), isActive: $isDisplayDetailView1) {
Text("Go to detail view 1")
}
}
}
.navigationBarTitle("Menu")
.listStyle(GroupedListStyle())
DetailView1()
}
}
}
Obviously, I tried to add edgesIgnoringSafeArea(.top) to my NavigationView view (and to the List one, and to the DetailView1 one,...), but it doesn't change anything... Please help me, I don't understand where is the problem!
Is there a reason you are using ScenePhase
You can just add this code at file scope
extension UINavigationController {
override open func viewDidLoad() {
super.viewDidLoad()
let standard = UINavigationBarAppearance()
standard.backgroundColor = .blue
let compact = UINavigationBarAppearance()
compact.backgroundColor = .green
let scrollEdge = UINavigationBarAppearance()
scrollEdge.backgroundColor = .red
navigationBar.standardAppearance = standard
navigationBar.compactAppearance = compact
navigationBar.scrollEdgeAppearance = scrollEdge
}
}

SwiftUI Repaint View Components on Device Rotation

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

Show a new View from Button press Swift UI

I would like to be able to show a new view when a button is pressed on one of my views.
From the tutorials I have looked at and other answered questions here it seems like everyone is using navigation button within a navigation view, unless im mistaken navigation view is the one that gives me a menu bar right arrows the top of my app so I don't want that. when I put the navigation button in my view that wasn't a child of NavigationView it was just disabled on the UI and I couldn't click it, so I guess I cant use that.
The other examples I have seen seem to use presentation links / buttons which seem to show a sort of pop over view.
Im just looking for how to click a regular button and show another a view full screen just like performing a segue used to in the old way of doing things.
Possible solutions
1.if you want to present on top of current view(ex: presentation style in UIKit)
struct ContentView: View {
#State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
DetailView()
}
}
}
2.if you want to reset current window scene stack(ex:after login show home screen)
Button(action: goHome) {
HStack(alignment: .center) {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
func goHome() {
if let window = UIApplication.shared.windows.first {
window.rootViewController = UIHostingController(rootView: HomeScreen())
window.makeKeyAndVisible()
}
}
3.push new view (ex: list->detail, navigation controller of UIKit)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}.navigationBarTitle("Navigation")
}
}
}
}
4.update the current view based on #state property, (ex:show error message on login failure)
struct ContentView: View {
#State var error = true
var body: some View {
...
... //login email
.. //login password
if error {
Text("Failed to login")
}
}
}
For simple example you can use something like below
import SwiftUI
struct ExampleFlag : View {
#State var flag = true
var body: some View {
ZStack {
if flag {
ExampleView().tapAction {
self.flag.toggle()
}
} else {
OtherExampleView().tapAction {
self.flag.toggle()
}
}
}
}
}
struct ExampleView: View {
var body: some View {
Text("some text")
}
}
struct OtherExampleView: View {
var body: some View {
Text("other text")
}
}
but if you want to present more view this way looks nasty
You can use stack to control view state without NavigationView
For Example:
class NavigationStack: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var list: [AuthState] = []
public func push(state: AuthState) {
list.append(state)
didChange.send()
}
public func pop() {
list.removeLast()
didChange.send()
}
}
enum AuthState {
case mainScreenState
case userNameScreen
case logginScreen
case emailScreen
case passwordScreen
}
struct NavigationRoot : View {
#EnvironmentObject var state: NavigationStack
#State private var aligment = Alignment.leading
fileprivate func CurrentView() -> some View {
switch state.list.last {
case .mainScreenState:
return AnyView(GalleryState())
case .none:
return AnyView(LoginScreen().environmentObject(state))
default:
return AnyView(AuthenticationView().environmentObject(state))
}
}
var body: some View {
GeometryReader { geometry in
self.CurrentView()
.background(Image("background")
.animation(.fluidSpring())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height,
alignment: self.aligment))
.edgesIgnoringSafeArea(.all)
.onAppear {
withAnimation() {
switch self.state.list.last {
case .none:
self.aligment = Alignment.leading
case .passwordScreen:
self.aligment = Alignment.trailing
default:
self.aligment = Alignment.center
}
}
}
}
.background(Color.black)
}
}
struct ExampleOfAddingNewView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.push(state: .emailScreen) }){
Text("Tap me")
}
}
}
}
struct ExampleOfRemovingView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.pop() }){
Text("Tap me")
}
}
}
}
In my opinion this bad way, but navigation in SwiftUI much worse