issue with if #available SwiftUI less than iOS 14 - swiftui

I'm using the beta version of Xcode 12.0 to play around with LazyVGrid, to render this grid within a scrollview if the phone has iOS 14, otherwise just to render the ScrollView as one column.
When I launch this on the app on my phone (not using iOS 14), opening this view causes my app to crash. But if I comment out the "if #available" section and just display what's in the "else" statement, it works fine.
Is there an issue with if #available in earlier versions of iOS or is my syntax just incorrect?
var body: some View {
NavigationView {
VStack {
//Empty View navigation link to choose the selected pack in User Defaults.
ScrollView(.vertical, showsIndicators: false) {
//Checks if iOS version 14.0 is available to render the lazy grid view
if #available(iOS 14.0, *) {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 15) {
//checks if the pack is in the purchased list - if so, renders it as an unlocked tile.
ForEach((allPacks), id: \.self) { pack in
UnlockedPackTile(tilePack: pack)
.onTapGesture {
print("Originally tapped \(pack.name)")
self.userInformation.defaultPack = pack
self.isPresented.toggle()
}
}
}
} else {
//does this as a simple stack instead if iOS 14 is not available.
ForEach((allPacks), id: \.self) { pack in
UnlockedPackTile(tilePack: pack)
.onTapGesture {
print("Originally tapped \(pack.name)")
self.userInformation.defaultPack = pack
self.isPresented.toggle()
}
}
}
}

Try to wrap content of ScrollView into Group (or VStack) like
ScrollView(.vertical, showsIndicators: false) {
Group {
if #available(iOS 14.0, *) {
// ... new content here
} else {
// ... old content here
}
}
}

Related

How To Change List Color in SwiftUI on iOS 16 with Minimum Deployment of iOS 14

I've developed an app in SwiftUI using a minimum deployment of iOS 14. I have a custom background color which I've always created using a ZStack approach. This has worked well until iOS 16 and Xcode 14.
Historically this worked.
struct ContentView: View {
var body: some View {
UITableView.appearance().backgroundColor = .clear
UICollectionView.appearance().backgroundColor = .clear
return ZStack {
Color(.blue)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Hello World")
List {
Text("Item1")
Text("Item2")
} // List
.listStyle(InsetGroupedListStyle())
} // VStack
} // ZStack
}
}
When this is built in iOS 16 in the simulator I see the following. On device the background color is correct. I don't like that the simulator shows the wrong color. It could be a bug in the simulator but this doesn't seem right.
In iOS 16 the new fix is to place the following modifier on the List.
.scrollContentBackground(.hidden)
Full code for iOS 16.
struct ContentView: View {
var body: some View {
UITableView.appearance().backgroundColor = .clear
UICollectionView.appearance().backgroundColor = .clear
return ZStack {
Color(.blue)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Hello World")
List {
Text("Item1")
Text("Item2")
} // List
.listStyle(InsetGroupedListStyle())
.scrollContentBackground(.hidden)
} // VStack
} // ZStack
}
}
This works great. Image below.
My issue is that with a minimum deployment of iOS 14 the added scrollContentBackground(.hidden) throws an error stating this is only available in iOS 16. Any thoughts on working around this with iOS 16 with a minimum deployment of iOS 14 would be greatly appreciated.
I followed the tutorial referenced above and wanted to provide the code below for anyone with the same question.
import Foundation
import SwiftUI
public struct Backport<Content> {
public let content: Content
public init(_ content: Content) {
self.content = content
}
}
extension View {
var backport: Backport<Self> { Backport(self) }
}
extension Backport where Content: View {
#ViewBuilder func scrollContentBackground(_ visibility: Visibility) -> some View {
if #available(iOS 16, *) {
content.scrollContentBackground(visibility)
} else {
content
}
}
}
In use on List
var body: some View {
UITableView.appearance().backgroundColor = .clear
UICollectionView.appearance().backgroundColor = .clear
return ZStack {
Color(.blue)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Hello World")
List {
Text("Item1")
Text("Item2")
} // List
.listStyle(InsetGroupedListStyle())
.backport.scrollContentBackground(.hidden)
} // VStack
} // ZStack
}
This works perfectly.

GeometryReader acting weird when presenting a modal on iOS 16. Bug or new behavior?

I'm seeing a weird behavior that is affecting one of my views in SwiftUI after upgrading to iOS 16.
Just to give some context, here is the stack:
Xcode 14
Simulator or real device on iOS 15.5 and 16
Considering the minimum reproducible code below:
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
GeometryReader { reader in
VStack(spacing: 36) {
Text("Screen frame:\n\(String(describing: reader.frame(in: .global)))")
.multilineTextAlignment(.center)
Button {
isPresented.toggle()
} label: {
Text("Open modal")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.onReceive(NotificationCenter.default.appDidBecomeActive()) { _ in
print(reader.frame(in: .global))
}
.onReceive(NotificationCenter.default.appDidEnterBackground()) { _ in
print(reader.frame(in: .global))
}
}
.sheet(isPresented: $isPresented) {
modalView
}
}
private var modalView: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}
extension NotificationCenter {
func appDidBecomeActive() -> AnyPublisher<Notification, Never> {
publisher(for: UIApplication.didBecomeActiveNotification).eraseToAnyPublisher()
}
func appDidEnterBackground() -> AnyPublisher<Notification, Never> {
publisher(for: UIApplication.didEnterBackgroundNotification).eraseToAnyPublisher()
}
}
As soon the view starts, it's possible to see the frame available due to the GeometryReader. Then following the steps:
Open the modal view
Send the app to the background
Open the app again
Close the modal
It's possible to see that the frame changed, and the values match with the 3D effect when a view is presenting another view, and it's never changing again to the right values unless you send the app again to the background or switch views (e.g. using a TabView).
I don't find anything on iOS release notes talking about it, so I supposed it must be a bug (I've filled out a bug report already).
On iOS 15, the frame value keeps stable at the same value.
I have a couple of views relying on the value of a GeometryReader, and it's causing my view to deform because of this issue. Does anyone know a way to force the recalculation for the GeometryReader for this case?
Any help is appreciated.
The issue won't occur if you control the display of the sheet with the new presentationDetents method, provided you do not request to cover the entire screen.
I modified your code as follows:
.sheet(isPresented: $isPresented) {
if #available(iOS 16, *) {
modalView
.presentationDetents([.fraction(0.99)])
}
else {
modalView
}
}
The issue will remain if you request .fraction(1), i.e. covering the whole screen.

SwiftUI Constraint Issue For NavigationTitle in iOS 15

I'm having an issue with Xcode throwing a constraint message in the console every time I use a NavigationTitle.
I'd like to say that I've read through the following post and have tried adding StackedNavigationViewStyle(). This worked for iOS 14 and Xcode 12.3. Since upgrading to iOS 15 and Xcode 13 the solution stops working.
SwiftUI NavigationView navigationBarTitle LayoutConstraints issue
The message is.
[LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one
you don't want. Try this: (1) look at each constraint and try to
figure out which you don't expect; (2) find the code that added the
unwanted constraint or constraints and fix it. (
"<NSLayoutConstraint:0x28189d540 UIView:0x155f53c50.trailing == _UIBackButtonMaskView:0x155f538f0.trailing (active)>",
"<NSLayoutConstraint:0x281890af0 'Mask_Trailing_Trailing' _UIBackButtonMaskView:0x155f538f0.trailing == _UIButtonBarButton:0x155f52d00.trailing (active)>",
"<NSLayoutConstraint:0x2818912c0 'MaskEV_Leading_BIB_Trailing' H:[_UIModernBarButton:0x155f53610]-(0)-[UIView:0x155f53c50]
(active)>",
"<NSLayoutConstraint:0x281890c30 'UINav_static_button_horiz_position'
_UIModernBarButton:0x155f53610.leading == UILayoutGuide:0x2802a5ce0'UIViewLayoutMarginsGuide'.leading
(active)>",
"<NSLayoutConstraint:0x281890640 'UINavItemContentGuide-leading' H:[_UIButtonBarButton:0x155f52d00]-(6)-[UILayoutGuide:0x2802a5dc0'UINavigationBarItemContentLayoutGuide']
(active)>",
"<NSLayoutConstraint:0x28189dd60 'UINavItemContentGuide-trailing' UILayoutGuide:0x2802a5dc0'UINavigationBarItemContentLayoutGuide'.trailing
== _UINavigationBarContentView:0x155f51f30.trailing (active)>",
"<NSLayoutConstraint:0x28189b340 'UIView-Encapsulated-Layout-Width'
_UINavigationBarContentView:0x155f51f30.width == 0 (active)>",
"<NSLayoutConstraint:0x28189e300 'UIView-leftMargin-guide-constraint'
H:|-(8)-UILayoutGuide:0x2802a5ce0'UIViewLayoutMarginsGuide'
(active, names: '|':_UINavigationBarContentView:0x155f51f30 )>" )
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x28189d540 UIView:0x155f53c50.trailing ==
_UIBackButtonMaskView:0x155f538f0.trailing (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints
to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView
listed in <UIKitCore/UIView.h> may also be helpful.
When adding StackedNavigationViewStyle to an iOS 15 app the Title and TabBar become inactive and the list moves underneath.
The code for this is below.
struct Tab1: View {
var body: some View {
// // Used for PacificBlue color.
UITableView.appearance().backgroundColor = .clear
// UITableView.appearance().separatorColor = UIColor(Color.white)
return NavigationView {
ZStack {
Color.pacificBlue
.edgesIgnoringSafeArea(.all)
List {
ForEach(1..<100) { index in
if #available(iOS 15.0, *) {
NavigationLink(destination: DetailView()) {
HStack {
Text("Row \(index)")
//
.listRowSeparatorTint(Color.pink)
}
.background(Color.pacificBlue)
} // NavigationLink
.listRowSeparatorTint(Color.pink)
} else {
// Fallback on earlier versions
NavigationLink(destination: DetailView()) {
Text("Row \(index)")
} // NavigationLink
} // if else
} // ForEach
.listRowBackground(Color.pacificBlue)
} // List
.listStyle(PlainListStyle())
} // ZStack
.navigationTitle("Title")
} // NavigationView
.navigationViewStyle(StackNavigationViewStyle())
} // View
}
If built on iOS 14 the expected behavior is seen.
Any help would be appreciated.
Recently, I was challenged by the same issue of NavigationBar and TabBar becoming transparent and unresponsive to scroll with IOS 15.0 exclusively, while IOS 14 and IOS 15.2 and above had normal behavior. After many hours of identifying the problem, I discovered that it caused by combination of .navigationViewStyle(.stack) and the ZStack that's used inside to provide Color as background. I have no explanation why it behaves like that, neither why the solutions work.
Option 1: Wrap the NavigationView with another ZStack
struct ContentView: View {
var body: some View {
UITableView.appearance().backgroundColor = .clear
return ZStack {
NavigationView {
ZStack {
Color.blue
.edgesIgnoringSafeArea(.all)
List {
ForEach(1...50, id:\.self) { index in
Text("Row \(index)")
}
}
}
.navigationTitle("Title")
}
.navigationViewStyle(.stack)
}
}
}
Option 2: Apply the ZStack only to IOS 15 with a ViewModifier
struct ContentView: View {
var body: some View {
UITableView.appearance().backgroundColor = .clear
return NavigationView {
ZStack {
Color.blue
.edgesIgnoringSafeArea(.all)
List {
ForEach(1...50, id:\.self) { index in
Text("Row \(index)")
}
}
}
.navigationTitle("Title")
}
.navigationViewStyle(.stack)
.modifier(NavigationModifier())
}
}
struct NavigationModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 15.0, *) {
ZStack {
content
}
} else {
content
}
}
}
I had the same problem using NavigationView and TabView together. Until iOS 14 I used the code bellow and it worked without problem:
NavigationView{
TabView(selection: $selection) {
...
}
}
However, since iOS 15, the same code started having the same navigationTitle you're having.
After a lot of time searching for a solution I found that if we change the order it works perfectly:
struct ContentView: View {
var body: some View {
TabView(selection: $selection) {
NavigationView {
Text("Example 1")
.navigationTitle("One")
}
.tabItem {
Text("One")
}
.tag("One")
NavigationView {
Text("Example 2")
.navigationTitle("Two")
}
.tabItem {
Text("Two")
}
.tag("Two")
}
}
}

SwiftUI LazyVGrid not showing two columns

I am playing with LazyVGrid in my beta version of Xcode, and I'm trying to make a vertical ScrollView show up with two columns instead of one. I have this view set up to include a ScrollView that checks if iOS 14 is available, and if so, renders the view as a two column grid (I put the columns inside this instance of LazyVGrid rather than as an external property because I didn't want to have to mark the whole View with the #available property - although maybe I should).
However, when I preview the view, it shows up with only one column. Is there anything in my setup that might be causing my view to only show up with one column instead of two?
Related - is there an easy way to preview in Xcode with different builds that do/don't include iOS 14? (In case my issue is that my preview version doesn't have iOS 14).
Here is my view for reference:
struct PackPage: View {
#Binding var isPresented: Bool
var allPacks: [Pack] = [samplePack, couplesPack, roadTripPack, familyPack]
var purchasedPacks: [Pack] = [samplePack]
var unpurchasedPacks: [Pack] = [couplesPack, roadTripPack, familyPack]
let userDefaults = UserDefaults.standard
#State private var action: Int? = 0
#State private var linkLock: Pack? = samplePack
private func isPurchased(pack: Pack, allPacks: [Pack]) -> Bool {
if allPacks.contains(pack) {
return true
} else {
return false
}
}
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: PurchasePage(pack: linkLock!), tag: 1, selection: $action) {
EmptyView()
}
ScrollView(.vertical, showsIndicators: false) {
ForEach((allPacks), id: \.self) { pack in
//Checks if iOS version 14.0 is available to render the lazy grid view
if #available(iOS 14.0, *) {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 20) {
if
//checks if the pack is in the purchased list - if so, renders it as an unlocked tile.
isPurchased(pack: pack, allPacks: purchasedPacks) {
UnlockedPackTile(tilePack: pack)
.onTapGesture {
print("Originally tapped \(pack.name)")
self.userInformation.defaultPack = pack
self.isPresented.toggle()
}
} else {
// if pack is not purchased, renders it as a locked pack tile
LockedPackTile(tilePackLocked: pack)
.onTapGesture {
self.linkLock = pack
self.action = 1
}
}
}
} else {
//does this as a simple VStack instead if iOS 14 is not available.
if isPurchased(pack: pack, allPacks: purchasedPacks) {
UnlockedPackTile(tilePack: pack)
.onTapGesture {
print("Originally tapped \(pack.name)")
self.userInformation.defaultPack = pack
self.isPresented.toggle()
}
} else {
LockedPackTile(tilePackLocked: pack)
.onTapGesture {
self.linkLock = pack
self.action = 1
}
}
}
}
}
Text("Button")
.navigationBarTitle("Question Packs")
}
}
}
}
You have to put ForEach inside LazyVGrid, so latter arranges dynamic views:
ScrollView(.vertical, showsIndicators: false) {
//Checks if iOS version 14.0 is available to render the lazy grid view
if #available(iOS 14.0, *) {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 20) {
ForEach((allPacks), id: \.self) { pack in

How to set button focus in SwiftUI?

!!! TVOS !!!
I have list of buttons and it somehow autofocus first button in that list (when view is loaded). Is there any way how to focus another button in that list using SwiftUI?
I know there is preferredFocusedView in UIKit but id like to do this in SwiftUI.
Thanks for any advice!
SwiftUI 2.0
Note: !! Partial solution !!
The following approach, as tested with Xcode 12b5 / tvOS 14 works only with stacks and does not work (in any tested combination) for List/ScrollView.
Anyway for small on-screen sets of buttons it is applicable so worth posting.
struct DemoPreferredFocusView: View {
#Namespace var ns
#Environment(\.resetFocus) var resetFocus
#AppStorage("initialButton") var initialButton: Int = 3
var body: some View {
VStack {
ForEach(0..<10, id: \.self) { i in
Button(action: {
print(">> tapped \(i)")
self.initialButton = i
}) {
Text("Button \(i)")
}
.prefersDefaultFocus(i == initialButton, in: ns)
}.focusScope(ns)
}
.onAppear {
DispatchQueue.main.async {
self.resetFocus.callAsFunction(in: ns)
}
}
}
}