Supporting iOS 16 widget while maintaining compatibility with iOS 15 [duplicate] - swiftui

How can we add a Lock Screen Widget to an existing Widget Bundle and still support iOS 15? :thinking_face: for ex this won't compile
struct SecondExtraBundle: WidgetBundle {
#WidgetBundleBuilder
var body: some Widget {
DailyHeartRatesWidget()
if #available(iOSApplicationExtension 16.0, *) {
LockScreenRecoveryScoreWidget()//Requires iOS 16?
} else {
// Fallback on earlier versions
}
}
}

I think this should work. Simply return an EmptyWidgetConfiguration in case the widget is not supported.
struct SomeWidgetBundle: WidgetBundle {
#WidgetBundleBuilder
var body: some Widget {
AlwaysAvailableWidget()
LockScreenWidget()
}
}
struct LockScreenWidget: Widget {
var body: some WidgetConfiguration {
if #available(iOSApplicationExtension 16.0, *) {
return StaticConfiguration(
kind: "some.kind",
provider: LockScreenWidgetTimelineProvider()
) { provider in
Text("Some view")
}
.configurationDisplayName("Some display name")
.description("Some description")
.supportedFamilies([.accessoryCircular])
} else {
return EmptyWidgetConfiguration()
}
}
}
This did not work before Xcode 14, but SE-0360 is already implemented and you can do this now.

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.

How can I remove the #available tag in the this Swift struct?

I have the following code in my WidgetBundle class, but I want to remove the #available tag since as is, if the user is not iOS 16, they won't have access to Home Screen or Lock Screen widgets at all.
struct WidgetBundler: WidgetBundle {
#WidgetBundleBuilder
var body: some Widget {
HomeScreenWidget()
LockScreenWidget()
}
}
The compiler does not like this code, and it was the only other way I could think of to get around this issue:
struct WidgetBundler: WidgetBundle {
#WidgetBundleBuilder
var body: some Widget {
if #available(iOSApplicationExtension 16, *) {
HomeScreenWidget()
LockScreenWidget()
} else {
HomeScreenWidget()
}
}
}
How do I remove the #available tag and get it so users can access the Home Screen widgets even on iOS 14 and 15?
This solution works:
#main
struct WidgetsBudle: WidgetBundle {
var body: some Widget {
if #available(iOSApplicationExtension 16.0, *) {
return ios16Widgets
} else {
return ios14Widgets
}
}
#WidgetBundleBuilder
var ios14Widgets: some Widget {
HomeScreenWidget()
}
#WidgetBundleBuilder
var ios16Widgets: some Widget {
HomeScreenWidget()
LockScreenWidget()
}
}
You can use this approach mentioned in avanderlee
#main
struct WidgetsBudler: WidgetBundle {
#WidgetBundleBuilder
var body: some Widget {
widgets()
}
func widgets() -> some Widget {
if #available(iOS 16.0, *) {
return WidgetBundleBuilder.buildBlock(HomeScreenWidget(),
LockScreenWidget())
} else {
return HomeScreenWidget()
}
}
}

SwiftUI: fullScreenCover with no animation?

I have this view:
struct TheFullCover: View {
#State var showModal = false
var body: some View {
Button(action: {
showModal.toggle()
}) {
Text("Show Modal")
.padding()
.foregroundColor(.blue)
}
.background(Color(.white))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.red, lineWidth:1)
)
.fullScreenCover(isPresented: $showModal, onDismiss: {
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
})
}
}
Every time I press the Button, the modal screen comes up fullscreen. All works great.
Question:
How do I disable the slide up animation? I want the view to be presented immediately fullscreen without animating to it.
Is there a way to do that?
A possible solution is to disable views animation completely (and then, if needed, enable again in .onAppear of presenting content), like
Button(action: {
UIView.setAnimationsEnabled(false) // << here !!
showModal.toggle()
}) {
and then
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
.onAppear {
UIView.setAnimationsEnabled(true) // << here !!
}
})
Tested with Xcode 13 / iOS 15
AFAIK the proper to do it as of today is using transaction https://developer.apple.com/documentation/swiftui/transaction
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
showModal.toggle()
}
I also created a handy extension for this:
extension View {
func withoutAnimation(action: #escaping () -> Void) {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
action()
}
}
}
which can be used like this:
withoutAnimation {
// do your thing
}
.fullScreenCover(isPresented: isPresented) {
content()
.background(TransparentBackground())
}
.transaction({ transaction in
transaction.disablesAnimations = true
})
this should work, based on #asamoylenko's answer
At the moment, I find it easier to use UIKit for presentation in SwiftUI.
someView
.onChange(of: isPresented) { _ in
if isPresented {
let vc = UIHostingController(rootView: MyView())
vc.modalPresentationStyle = .overFullScreen
UIApplication.shared.rootVC?.present(vc, animated: false)
} else {
UIApplication.shared.rootVC?.dismiss(animated: false)
}
}
Why not use an overlay instead?
.overlay {
if isLoading {
ZStack {
ProgressView()
}
.background(BackgroundCleanerView())
}
}

Conditional modifiers with #available not executing correctly

I've got a custom modifier to replace navigation bar title with an image view, in iOS 14 this is pretty straightforward with the .toolbar modifier, however in iOS 13 it needs a bit more work but it's possible.
The problem comes when I want to use both solutions in a conditional modifier, the following code reproduces the issue, it works when running on iOS 14 but it produces no result on iOS 13, however if the "#available" condition is removed from the modifier leaving only iOS 13 code, it works as expected. Wrapping iOS 14 in AnyView does not help either:
extension View {
#ViewBuilder
func configuresIcon() -> some View {
if #available(iOS 14.0, *){
self.modifier(NavigationConfigurationView14Modifier())
} else {
self.modifier(NavigationConfigurationViewModifier(configure: { nv in
nv.topItem?.titleView = UIImageView(image: UIImage(named: IMAGE_NAME_HERE))
}))
}
}
}
struct NavigationConfigurationViewModifier: ViewModifier {
let configure: (UINavigationBar) -> ()
func body(content: Content) -> some View {
content.background(NavigationControllerLayout(configure: {
configure($0.navigationBar)
}))
}
}
#available(iOS 14.0, *)
struct NavigationConfigurationView14Modifier: ViewModifier {
func body(content: Content) -> some View {
content
.toolbar {
ToolbarItem(placement: .principal) {
Image(IMAGE_NAME_HERE)
}
}
}
}
struct NavigationControllerLayout: UIViewControllerRepresentable {
var configure: (UINavigationController) -> () = { _ in }
func makeUIViewController(
context: UIViewControllerRepresentableContext<NavigationControllerLayout>
) -> UIViewController {
UIViewController()
}
func updateUIViewController(
_ uiViewController: UIViewController,
context: UIViewControllerRepresentableContext<NavigationControllerLayout>
) {
if let navigationContoller = uiViewController.navigationController {
configure(navigationContoller)
}
}
}
Try to wrap content in Group (not tested - only idea)
extension View {
#ViewBuilder
func configuresIcon() -> some View {
Group {
if #available(iOS 14.0, *){
self.modifier(NavigationConfigurationView14Modifier())
} else {
self.modifier(NavigationConfigurationViewModifier(configure: { nv in
nv.topItem?.titleView = UIImageView(image: UIImage(named: IMAGE_NAME_HERE))
}))
}
}
}
}
Maybe it will work if we check OS version at runtime, not at compiletime (because buildlimitedavailability(_:) is itself available only since iOS 14).
extension View {
func erase() -> AnyView {
return AnyView(self)
}
func applyIf<VM1: ViewModifier, VM2: ViewModifier>(_ condition: #autoclosure () -> Bool, ApplyIfTrue: VM1, ApplyIfFalse: VM2
) -> AnyView {
if condition() {
return self.modifier(ApplyIfTrue).erase()
} else {
return self.modifier(ApplyIfFalse).erase()
}
}
#ViewBuilder func configuresIcon() -> some View {
self.applyIf(NSProcessInfo().isOperatingSystemAtLeastVersion(NSOperatingSystemVersion(majorVersion: 14, minorVersion: 0, patchVersion: 0)),
ApplyIfTrue: NavigationConfigurationView14Modifier(),
ApplyIfFalse: modifier(NavigationConfigurationViewModifier(configure: { nv in
nv.topItem?.titleView = UIImageView(image: UIImage(named: IMAGE_NAME_HERE))
})))
}
}
If you really need #available option there are several things to try. 1) Maybe it is because of inappropriate extension point, so try to move it out of View's extension. 2)Move the logic from modifiers to the body.
For example the code below work fine.
#available(macOS 10.15, iOS 13.0, *)
struct ContentView: View {
var body: some View {
ScrollView {
if #available(macOS 11.0, iOS 14.0, *) {
LazyVStack {
ForEach(1...1000, id: \.self) { value in
Text("Row \(value)")
}
}
} else {
VStack {
ForEach(1...1000, id: \.self) { value in
Text("Row \(value)")
}
}
}
}
}
}

How do I present a SwiftUI context menu conditionally?

Consider the following view code:
Text("Something")
.contextMenu {
// Some menu options
}
This works fine. What I would like to do: present the contextMenu through a view modifier indirection. Something like this:
Text("Something")
.modifier(myContextMenu) {
// Some menu options
}
Why: I need to do some logic inside the modifier to conditionally present or not present the menu. I can’t work out the correct view modifier signature for it.
There is another contextMenu modifier available which claims that I can conditionally present the context menu for it. Upon trying this out, this does not help me, because as soon as I add contextMenu modifier to a NavigationLink on iOS, the tap gesture on it stops working. There is discussion in a response below.
How do I present a context menu using a view modifier?
Here is a demo for optional context menu usage (tested with Xcode 11.2 / iOS 13.2)
struct TestConditionalContextMenu: View {
#State private var hasContextMenu = false
var body: some View {
VStack {
Button(hasContextMenu ? "Disable Menu" : "Enable Menu")
{ self.hasContextMenu.toggle() }
Divider()
Text("Hello, World!")
.background(Color.yellow)
.contextMenu(self.hasContextMenu ?
ContextMenu {
Button("Do something1") {}
Button("Do something2") {}
} : nil)
}
}
}
Something like this?
Text("Options")
.contextMenu {
if (1 == 0) { // some if statements here
Button(action: {
//
}) {
Text("Choose Country")
Image(systemName: "globe")
}
}
}
Here’s what I came up with. Not entirely satisfied, it could be more compact, but it works as expected.
struct ListView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: ItemView(item: "Something")) {
Text("Something").modifier(withiOSContextMenu())
}.modifier(withOSXContextMenu())
}
}
}
}
struct withOSXContextMenu: ViewModifier {
func body(content: Content) -> some View {
#if os(OSX)
return content.contextMenu(ContextMenu {
ContextMenuContent()
})
#else
return content
#endif
}
}
struct withiOSContextMenu: ViewModifier {
func body(content: Content) -> some View {
#if os(iOS)
return content.contextMenu(ContextMenu {
ContextMenuContent()
})
#else
return content
#endif
}
}
func ContextMenuContent() -> some View {
Group {
Button("Click me") {
print("Button clicked")
}
Button("Another button") {
print("Another button clicked")
}
}
}