Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 12 days ago.
Improve this question
I'm targeting iOS 16 for my app in which I access the screen height and width using UIScreen.main.bounds.width and UIScreen.main.bounds.height so I can draw views based on these two values. I'm assigning these two values to two CGFloat properties in the view struct as follows:
struct ContentView: View {
var width: CGFloat = UIScreen.main.bounds.width
var height: CGFloat = UIScreen.main.bounds.height
var fontSize: CGFloat
var body: some View {
// draw views here using width and height properties
}
Xcode is showing a warning message saying 'main' will be deprecated in a future version of iOS: use a UIScreen instance found through context instead: i.e, view.window.windowScene.screen
I'm not sure how to apply the answer here to my use case and I don't want to use GeometryReader since it just messes up the overall layout.
Any suggestions on how to obtain screen width and height in an app targeting iOS 16 and above without using GeometryReader?
Swift 5.5
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
GeometryReader { proxy in
ContentView()
.environment(\.mainWindowSize, proxy.size)
}
}
}
}
struct ContentView: View {
#Environment(\.mainWindowSize) var windowSize
var body: some View {
ZStack {
Text("Hello, world!")
.frame(width: windowSize.width/2, height: windowSize.height)
.background(Color.blue)
}
}
}
private struct MainWindowSizeKey: EnvironmentKey {
static let defaultValue: CGSize = .zero
}
extension EnvironmentValues {
var mainWindowSize: CGSize {
get { self[MainWindowSizeKey.self] }
set { self[MainWindowSizeKey.self] = newValue }
}
}
There is a new feature in iOS 16: Users can duplicate an application. So it looks like the user has five applications, but only one is actually running. When that happens, things like UIScreen.main don't make sense anymore, because you are actually responsible for five apps!
Now if you have a view, that belongs to ONE of the five virtual apps, so view.window.windowScene.screen will be the UIScreen that is actually responsible for what the user sees (not the four other instances of the app in the background). In iOS 17, code like that will be deprecated. State that used to be global in iOS 15 isn't global anymore.
Take an extreme case: Two copies of an app, both running in split screen, one on the left third, one on the right two thirds of your screen. There is nothing "global" that you could use.
Related
The Problem
In our brand new SwiftUI project (Multiplatform app for iPhone, iPad and Mac) we are using a ScrollView with both .horizontal and .vertical axes enabled. In the end it all looks like a spreadsheet.
Inside of ScrollView we are using LazyVStack with pinnedViews: [.sectionHeaders, .sectionFooters]. All Footer, Header and the content cells are wrapped into a LazyHStack each. The lazy stacks are needed due to performance reasones when rows and columns are growing.
And this exactly seems to be the problem. ScrollView produces a real layout mess when scrolling in both axes at the same time.
Tested Remedies
I tried several workarounds with no success.
We built our own lazy loading horizontal stack view which shows only the cells whose indexes are in the visible frame of the scrollview, and which sets dynamic calculated leading and trailing padding.
I tried the Introspect for SwiftUI packacke to set usesPredominantAxisScrolling for macOS and isDirectionalLockEnabled for iOS. None of this works as expected.
I also tried a workaround to manually lock one axis when the other one is scrolling. This works fine on iOS but it doesn't on macOS due to extreme poor performance (I think it's caused by the missing NSScrollViewDelegate and scroll events are being propagated via NotificationCenter).
How does it look?
To give you a better idea of what I mean, I have screenshots for both iOS and macOS.
Here you can watch short screen recording for both iOS and macOS.
Example Code
And this is the sample code used for the screenshots. So you can just create a new Multiplatform project and paste the code below in the ContentView.swift file. That's all.
import SwiftUI
let numberOfColums: Int = 150
let numberOfRows: Int = 350
struct ContentView: View {
var items: [[Item]] = {
var items: [[Item]] = [[]]
for rowIndex in 0..<numberOfRows {
var row: [Item] = []
for columnIndex in 0..<numberOfColums {
row.append(Item(column: columnIndex, row: rowIndex))
}
items.append(row)
}
return items
}()
var body: some View {
Group {
ScrollView([.horizontal, .vertical]) {
LazyVStack(alignment: .leading, spacing: 1, pinnedViews: [.sectionHeaders, .sectionFooters]) {
Section(header: Header()) {
ForEach(0..<items.count, id: \.self) { rowIndex in
let row = items[rowIndex]
LazyHStack(spacing: 1) {
ForEach(0..<row.count, id: \.self) { columnIndex in
Text("\(columnIndex):\(rowIndex)")
.frame(width: 100)
.padding()
.background(Color.gray.opacity(0.2))
}
}
}
}
}
}
.edgesIgnoringSafeArea([.top])
}
.padding(.top, 1)
}
}
struct Header: View {
var body: some View {
LazyHStack(spacing: 1) {
ForEach(0..<numberOfColums, id: \.self) { idx in
Text("Col \(idx)")
.frame(width: 100)
.padding()
.background(Color.gray)
}
Spacer()
}
}
}
struct Item: Hashable {
let id: String = UUID().uuidString
var column: Int
var row: Int
}
Can You Help?
Okay, long story short...
Does anybody know a real working solution for this issue?
Is it a known SwiftUI ScrollView bug?
If there were a way to lock one scrolling direction while the other one is being scrolled, then I could deal with that. But at the moment, unfortunately, it is not usable for us.
So any solution is highly appreciated! 😃
Is there any reason you don't want to use a Grid for this layout?
Here is the article I would suggest you read to solve this problem:
https://swiftui-lab.com/impossible-grids/
I have an interesting situation in regards to animations in SwiftUI. In its simplified form, I have a view that shows a rectangle which, when tapped, should toggle a Bool binding and represent the change with an animated transition of color. But it seems like the animation doesn't happen when the view is inside a NavigationView and the binding is coming from a StateObject instead of simple local state. I can't explain why that would be the case, and would appreciate any thoughts.
Below is the code that shows a simplified case that reproduces the issue. The app code isn't particularly interesting; it's the default code that creates a WindowGroup and shows an instance of ContentView in it.
import SwiftUI
class AppState: ObservableObject {
#Published var isRed = false
}
struct RectView: View {
#Binding var isRed: Bool
var body: some View {
Rectangle()
.fill(isRed ? Color.red : Color.gray)
.frame(width: 75, height: 75, alignment: .center)
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) {
isRed.toggle()
}
}
}
}
struct ContentView: View {
#StateObject var appState = AppState()
#State private var childViewIsRed = false
var body: some View {
VStack {
NavigationView {
List {
NavigationLink("Link with binding to state object", destination: RectView(isRed: $appState.isRed))
NavigationLink("Link with binding to state variable", destination: RectView(isRed: $childViewIsRed))
}
}
.frame(height: 300)
RectView(isRed: $appState.isRed)
RectView(isRed: $childViewIsRed)
}
}
}
The gif/video below is me demonstrating four things, tapping on these views from bottom to top:
First I tap on the very bottom rectangle - the one with a binding to a #State property. It toggles with animation as expected. I tap again to leave it gray.
Then I tap the second rectangle from the bottom - one with a binding to a #Published property in the #StateObject. All is well.
Next, I tap on the NavigationLink that leads to a rectangle that is bound to the local #State property. When I tap on the rectangle the transition is animated fine. The very bottom rectangle also animates, which makes sense since they are bound to the same property.
Finally I tap on the top NavigationLink, which leads to a rectangle bound to the #Published property in the #StateObject. When I tap on this rectangle though, there is no animation. The rectangle snaps to red. The rectangle below it (which is bound to the same property) animates fine, proving that the property is indeed toggled. But there is no animation inside the NavigationView. Why? What am I missing?
I've searched for existing questions. I'm aware that there are some around NavigationView (like this) but that doesn't explain why one type of binding would work fine inside a NavigationView and another wouldn't. Similarly, there are ones around animating changes to #ObservedObjects (like this, would a #StateObject be similar?) but I don't follow why animating changes to such a binding would work fine outside of a NavigatonView and not inside one.
In case it's relevant I'm using Xcode 13.2.1, running on macOS 12.2.1, which is in turn running on a 16-inch M1 Max MacBook Pro. The simulator shown in the gif/video is an iPhone 11 Pro Max. I get the same results if I deploy this tiny test app to a physical iPhone 7 running iOS 15.3.1.
To be honest with you, I am not sure why, but utilizing the animation modifier on the RectView allows the animation to occur without any issues.
struct RectView: View {
#Binding var isRed: Bool
var body: some View {
Rectangle()
.fill(isRed ? Color.red : Color.gray)
.frame(width: 75, height: 75, alignment: .center)
.animation(.easeOut(duration: 1), value: isRed)
.onTapGesture { isRed.toggle() }
}
}
screen recording example
I'm creating a small SwiftUI app with a TabView, but the code stopped working as soon as I used the modifier .tabViewStyle(PageTabViewStyle()). Why is that? The app is meant to cycle through a series of images and is going to be similar in design to the Photos app. It's based on a Kavsoft tutorial, but that tutorial used deprecated code. When I used the more modern equivalent, the app's preview stopped working, and I can no longer see the TabView.
struct Home: View {
/* Posts */
#State var posts: [Post] = []
/* Cuurent Image */
#State var currentImage = ""
var body: some View {
/* Gallery */
TabView(selection: $currentImage) {
ForEach(posts) { post in
/* GeometryReader to get screen sizes for image */
GeometryReader { geo in
let size = geo.size
Image(post.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: size.width, height: size.height)
.cornerRadius(0)
}
.edgesIgnoringSafeArea(.all)
}
}
.tabViewStyle(PageTabViewStyle()) // Cannot work
.onAppear {
for index in 1...10 {
posts.append(Post(image: "\(index)"))
}
}
}
}
struct Post: Identifiable, Hashable {
var id = UUID().uuidString
var image: String
}
Adding an answer here for completeness, I suspected it would work with static content like Text(). This was crashing because ForEach is trying to load posts that don't exist yet, causing it to init from an empty array. For the tab view to work correctly, it needs at least one rendered view.
To fix this, posts needs to have at least one element in it to render.
I am currently using SwiftUI's Picker API, and would like to increase the Picker row's height. The reason for this is when text has a bigger font, the items overlap.
I am trying to avoid using UIPickerView + UIViewRepresentable if possible & would like to achieve this using purely SwiftUI if possible.
I have tried using Spacers, padding modifiers and frame but no luck as of yet.
I know this feature is already available on watchOS, but are there any alternatives when it comes to iOS? https://developer.apple.com/documentation/swiftui/link/defaultwheelpickeritemheight(_:)
I have attached basic picker code with the aforementioned issue. Thanks in advance.
struct ContentView: View {
#State private var selectedNum = 0
var body: some View {
Picker("", selection: self.$selectedNum) {
Text("1").font(.system(size: 80))
Text("2").font(.system(size: 80))
Text("3").font(.system(size: 80))
Text("4").font(.system(size: 80))
Text("5").font(.system(size: 80))
Text("6").font(.system(size: 80))
}
.pickerStyle(.wheel)
}
}
This is the result of the code
I'm trying to make a dynamic view to answer some questions. Those questions are created from my back office, ant the next question depends on the current user answer. So I have to build my view as things progress.
Here is a example :
First Question :
Are you alive ? : | Are you sure ?
|
YES/NO (no chosen) | YES/NO
I made a subview with the different type of question, and a container view to embed the path like that :
Here is the code of my container View, the white block is embed in a Scrollview
#ObservedObject var VM:SurveyBrowserViewModel
#State private var offsetX:CGFloat = 0
#State private var opacity:Double = 1
#State var myView:AnyView = AnyView(EmptyView())
var body: some View {
ZStack{
Image("background_survey_canyon")
.resizable()
backgroundTriangle
.opacity(0.65)
ScrollView(showsIndicators:false){
myView
.padding(.top, 55 + safeAreaTop)
.padding(.bottom, 60 + safeAreaBottom)
.offset(x : offsetX , y: 0)
.opacity(opacity)
.onReceive(VM.nextQuestionPassthrough) { (v) in
self.myView = v
}
.onReceive(VM.nextQuestionValuePassthrough) { (v) in
self.offsetX = v
self.opacity = 0
withAnimation(.spring()) {
self.offsetX = 0
self.opacity = 1
}
}
}
.frame(maxWidth: .infinity,maxHeight: .infinity)
}
}
and here is the viewModel section which updates the View state, current question is the question which needs to be displayed according the good type.
var nextQuestionPassthrough = PassthroughSubject<AnyView,Never>()
/// Get The good view to match the correct question tyoe
/// - Parameter answerSelected: Binding bool to hide/show next button visibility
private func getGoodView(){
guard (currentQuestion != nil) else { nextQuestionPassthrough.send(AnyView(EmptyView())); return }
switch QuestionType.intToQuestionType(value: currentQuestion!.type!){
case .YesOrNo :
if currentQuestionViewModel == nil{
currentQuestionViewModel = YesNoQuestionViewModel(question: currentQuestion!,temporaryId: temporaryId)
}
nextQuestionPassthrough.send(AnyView(YesNoQuestionView(VM: currentQuestionViewModel as! YesNoQuestionViewModel,answerSelected: nextViewState! )))
}
}
I've tried different way to achieve what I'm expecting :
I want a animation like a auto scroll coming form right to left, but it's cosmetic and it's not my main problem.
My problem is that when i send view from the nextQuestionPassthrough the init section from my new view is called but not the appear one. So all state are saved as we can see on the picture ( yes and no answer are two #State set to false at initialization but stay selected when I send the new view. I tried several ideas, like a published view instead of a #State but the behavior is the same.
I'm thinking about a new approch, with my container embed two views to make the good animation and the good life cycle ( init then appear )
Has anyone ever studied this problem or done something similar?