SwiftUI device orientation works on iPhone but not on iPad - swiftui

i'm playing around with SwiftUI and try to get correct screen size depending on orientation. The solution works fine in simulator on iPhone but lacks on iPad (i have no iPad for testing real). It seems on iPad it is just missing the redraw-event.
Is there a bug in SwiftUI or did I need to add some code?
struct ContentView: View {
#Environment(\.layoutDirection) var layoutDir
// verticalSizeClass initiate redrawing UI based on new direction (horSizeClass won't work)
#Environment(\.verticalSizeClass) var vertSizeClass
var body: some View {
VStack{
Text("UIDevice.current.orientation").font(.title)
if UIDevice.current.orientation.isPortrait {
Text("Portrait")
} else if UIDevice.current.orientation.isLandscape {
Text("Landscape")
} else if UIDevice.current.orientation.isFlat {
Text("is Flat")
}
Text("UIScreen.main.bounds").font(.title)
Text("Width:\(UIScreen.main.bounds.width)")
Text("Height:\(UIScreen.main.bounds.height)")
Text("#Environment(\\.horizontalSizeClass").font(.title)
if vertSizeClass == .regular {
Text("regular")
} else if vertSizeClass == .compact {
Text("compact")
} else {
Text("default")
}
}
}
}

Related

SwiftUI ScrollView horizontal scroll lag on macOS

Faced with a very strange ScrollView behavior on macOS. The content freezes under the mouse during horizontal scrolling. But it is worth taking the mouse away from the window and the content scrolls normally.
This happens when I try to use a vertical scroll inside a horizontal one:
struct ScrollTestView: View {
var body: some View {
ScrollView(.horizontal) {
ScrollView(.vertical) {
VStack {
ForEach(0..<20, id: \.self) { row in
HStack {
ForEach(0..<20, id: \.self) { item in
Text("\(item)")
.font(.title)
.padding()
.background {
Color.gray
}
}
}
}
}
}
}
}
}
Yes, I know that I can use the same ScrollView for both axes simultaneously, but I need solution with two ScrollViews because of desired UX.
This solution is perfectly works on iOS, but I have this strange behavior on macOS.
Also if you swap a horizontal and a vertical ScrollView in the exact same code, everything works just fine:
struct ScrollTestView: View {
var body: some View {
ScrollView(.vertical) {
ScrollView(.horizontal) {
// ...
}
}
}
}
Looks like this is a SwiftUI bug, but I am not sure, maybe I am missing something?
Any ideas?

View using if/else statement displays the wrong page - SwiftUI

I am new to SwiftUI and want to display a view based on a boolean value but every time I run my app in the simulator, it always runs the else part and displays ContentView instead of OnboardingView.
Kindly guide what I am doing wrong in the code.
import SwiftUI
#main
struct FirstApp: App {
#AppStorage("isOnboarding") var isOnboarding: Bool = true
var body: some Scene {
WindowGroup {
if isOnboarding {
OnboardingView()
} else
{
ContentView()
}
}
}
}
The code works fine. The "problem" is the persisted value in your device for key "isOnboarding" is false (the default value that you have assigned will work only the first them). To make sure that this statement is right, you can do a simple test: toggle the isOnboarding value and relaunch the app. Something like this:
#main
struct FirstApp: App {
#AppStorage("isOnboarding") var isOnboarding: Bool = true
var body: some Scene {
WindowGroup {
Toggle(isOn: $isOnboarding) {
Text("isOnboarding")
}
if isOnboarding {
OnboardingView()
} else {
ContentView()
}
}
}
}

Apply a navigationStyle based on horizontalSizeClass for the root NavigationView and preserve the navigation stack

On app launch, I want to get the horizontalSizeClass and based on if it's compact or regular, apply a navigation style to my root navigation view like so:
import SwiftUI
#main
struct MyApp: App {
#Environment(\.horizontalSizeClass) var sizeClass
var body: some Scene {
WindowGroup {
if sizeClass == .compact {
NavigationView {
Text("Compact size class inside stack navigation style")
}
.navigationViewStyle(StackNavigationViewStyle())
} else {
NavigationView {
Text("Regular size class inside default navigation style")
}
}
}
}
}
However, sizeClass always returns nil in this case.
How do I
determine if the horizontal size class is compact or regular on the root view, and
make the navigation style adapt to the size class any time it changes
My app is targeting iOS 14 for both iPhone and iPad.
Any help or a different approach to adapt for size class changes for the whole app is much appreciated.
Update 1
I tried the suggestions to use a ViewModifier or creating a custom view and adding the navigation in it's body like so:
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
MyRootView()
}
}
}
struct MyRootView: View {
#Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
if sizeClass == .compact {
NavigationView {
Text("Compact size class inside stack navigation style")
}
.navigationViewStyle(StackNavigationViewStyle())
} else {
NavigationView {
Text("Regular size class inside default navigation style")
}
}
}
}
However, the navigation stack pops to the root view every time the sizeClass changes. Is there a way to preserve the stack? For example: If the user is 5 levels deep in navigation, and sizeClass changes, change the navigation style while keeping the visible screen?
Thank you!
Update 2
I was able to find a WWDC session explaining exactly what I want, but it's in UIKit.
See 18:35 here: https://developer.apple.com/wwdc20/10105
I'm trying to achieve the same goal in SwiftUI (keep the screen the user selected while changing the size class to compact).
According to the session, UISplitViewController supports this because there's the concept of Restorable and Restore in the detail view. I can't find a way to do this in SwiftUI.
this setup works for me. I read somewhere in the docs that Environment are updated before a view is rendered.
I guess App is not a view.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
if horizontalSizeClass == .compact {
Text("Compact")
} else {
Text("Regular")
}
}
}
Yeah, forgot about the NavigationViews.
You could try something like this (using the code from "https://matteo-puccinelli.medium.com/conditionally-apply-modifiers-in-swiftui-51c1cf7f61d1")
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
extension View {
#ViewBuilder
func ifCondition<TrueContent: View, FalseContent: View>(_ condition: Bool, then trueContent: (Self) -> TrueContent, else falseContent: (Self) -> FalseContent) -> some View {
if condition {
trueContent(self)
} else {
falseContent(self)
}
}
}
struct ContentView: View {
#Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
NavigationView {
if sizeClass == .compact {
Text("Compact size class inside stack navigation style")
} else {
Text("Regular size class inside default navigation style")
}
}
.ifCondition(sizeClass == .compact) { nv in
nv.navigationViewStyle(StackNavigationViewStyle())
} else: { nv in
nv.navigationViewStyle(DefaultNavigationViewStyle())
}
}
}

Poor on-screen / scrolling performance using LazyVStack and LazyHStack with ScrollView in SwiftUI

I'm trying to draw a large scrolling SwiftUI View consisting of Rectangles that will be colour-coded. Step one is just testing 1,000s of rectangles on screen within a ScrollView, however, performance is awful. This code draws 16,000 rectangles, but the scrolling performance is really jittery and sometimes pauses a few seconds.
Any ideas on what is going on and how to fix appreciated.
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach (0..<32) { byte in
MemoryBlockView()
}
}
}
}
}
struct MemoryBlockView: View {
var body: some View {
LazyVStack(spacing: 2) {
ForEach (0..<8) { byte in
MemoryBlockRowView()
}
}
.padding(0)
}
}
struct MemoryBlockRowView: View {
var body: some View {
LazyHStack(spacing: 2) {
ForEach (0..<64) { byte in
Rectangle()
.fill(Color.blue)
.cornerRadius(2)
.padding(1)
}
}
}
}

Why does this SwiftUI LazyHStack update continuously?

I have a large set of URLs to images. I display the files' thumbnails in a LazyVStack. I have wrapped up the 'ThumbnailView' and the 'ThumbnailGenerator' in a struct and class respectively. However, when I ran the code I discovered that it kept re-initaiting the ThumbnailGenerators. After some investigation I found that after removing an HStack in the main view's hierarchy the problem went away.
Any thoughts as to why this might happen. (BTW I did log this with Apple, but still feel I am doing something wrong here myself.)
I have stripped the code back to the bare essentials here, replacing the thumbnail generation code with a simple sleep statement, to demonstrate the bug in action. Run it with the HStack in and it will print out the date continuously. Take it out and it works as expected.
#main
struct ExperimentApp: App {
var body: some Scene {
WindowGroup {
LazyVIssue()
.frame(width: 200, height: 140)
.padding(100)
}
}
}
struct LazyVIssue: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<10) { i in
HStack { /// <---- REMOVE THIS HSTACK AND IT WORKS
ThumbnailView()
Text("Filename \(i)")
}.padding()
}
}
}
}
}
struct ThumbnailView: View {
#StateObject private var thumbnailGenerator : ThumbnailGenerator
init() {
_thumbnailGenerator = StateObject(wrappedValue: ThumbnailGenerator())
}
var body: some View {
thumbnailGenerator.image
}
}
final class ThumbnailGenerator: ObservableObject {
var image : Image
init() {
print("Initiating", Date())
image = Image(systemName: "questionmark.circle.fill")
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
guard let self = self else { return }
sleep(1) /// Simulate some work to fetch image
self.image = Image(systemName: "camera.circle.fill")
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
}
I'm not sure why this is happening but I've seen had some funky things happen like this as well. If you initialize the ThumbnailGenerator() outside of the ThumbnailView init, I believe the issue goes away.
init(generator: ThumbnailGenerator) {
_thumbnailGenerator = StateObject(wrappedValue: generator)
}
Well, it is not clear for now what's going on here definitely (it is something about LazyVStack caching), but there is workaround - move everything into single row view.
Tested with Xcode 12.1 / iOS 14.1
struct LazyVIssue: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<10) { i in
ThumbnailView(i) // << single row view !!
}
}
}
}
}
struct ThumbnailView: View {
#StateObject private var thumbnailGenerator : ThumbnailGenerator
let row: Int
init(_ row: Int) {
self.row = row
_thumbnailGenerator = StateObject(wrappedValue: ThumbnailGenerator())
}
var body: some View {
HStack {
thumbnailGenerator.image
Text("Filename \(row)")
}.padding()
}
}