From ios 15 SWiftUI provides an action parameter for adding multiple options in alert, but for lesser OS versions how can we achieve that?
from iOS 15
view.alert("Choose a package", isPresented: $showAlert) {
Button("Standard") { package = "Standard" }
Button("Pro") { package = "Pro" }
Button("Premium") { package = "Premium" }
}
Any native option for iOS 14 or below?
Related
I have defined a DatePicker to have the compact style, as follows:
struct ContentView: View {
#State private var selectedDate = Date()
var body: some View {
DatePicker("Date Selected", selection: $selectedDate, displayedComponents: [.date])
.accessibilityIdentifier("DatePicker")
.datePickerStyle(.compact)
}
}
I have defined a UI test that taps on the DatePicker to show the DatePicker popup, then taps on one of the dates in the DatePicker popup to select that date, and then taps on the DatePicker again to dismiss the DatePicker popup, as follows:
func test_date_picker() {
let application = XCUIApplication()
// 0. Launch the app
application.launch()
// 1. Show the DatePicker popup
application.datePickers["DatePicker"].tap()
// 2. Change the selected date
application.datePickers.collectionViews.buttons["Friday, November 25"].tap()
// 3. Dismiss the DatePicker popup
application.datePickers["DatePicker"].tap()
}
This test works on iOS 14 and iOS 15 devices. Sadly, the final application.datePickers["DatePicker"].tap() call is failing on iOS 16 devices because application.datePickers["DatePicker"] is not hittable. How do I dismiss the DatePicker popup on iOS 16 devices?
For what it's worth, I'm running the test via Xcode 14.1 on iOS 16.1 simulator devices. I do not have a real iOS 16 device to hand so I cannot verify the behaviour on a real iOS 16 device.
Lastly, you can find a minimal application project that demonstrates the problem here.
I posted this same question on the Apple Developer Forums (here) and got back a response that has worked for me.
The idea is to define a forceTap() extension function on the XCUIElement class, as follows:
extension XCUIElement {
func forceTap() {
if (isHittable) {
tap()
} else {
coordinate(withNormalizedOffset: CGVector(dx:0.0, dy:0.0)).tap()
}
}
}
Explanation: If the XCUIElement instance is hittable, then the XCUIElement instance's tap() function is called. Otherwise, an XCUICoordinate instance is created for a point within the XCUIElement instance and that XCUICoordinate instance's tap() function is called instead.
The final line of my test function is now changed to the following:
application.datePickers["DatePicker"].forceTap()
I am building custom long-look notifications in an Apple Watch app, but for some reason the title defined in the notification's UNMutableNotificationContent is not showing on the long-look notification, and the custom sashColor I’m defining is not used.
In the “Transition to the Long-Look Interface” section of Presenting Notifications on Apple Watch on Apple’s website, there is a screenshot that shows what I would expect to see: a title for the notification in the sash, and a custom sash color.
I built an example app (code below) to isolate the issue.
Here is a screenshot of the notification in my app:
I expect to see the title (“Take Action!”) where the line is, and the sashColor as the background color for the circled region, based on my code.
The short-look of the notification does show the title briefly before it transitions to the long-look (it was hard to get a good screenshot, but here is one as it was animating into the long-look):
Showing or hiding the notification title is not mentioned anywhere that I can find in the documentation, so I expect that to show up automatically since it’s part of the notification.
For the sashColor override, I referred to Customizing Your Long-Look Interface on Apple’s website.
Is there something else specific I need to do to show the title on my customized long-look notification, and get sashColor to work?
Example App
To recreate the issue, create a watchOS app with companion iOS app in Xcode. I called it CustomWatchNotifications.
I updated the main iOS app file to this, with a simple class to request notification permission and send a test notification, which gets passed into the view:
import SwiftUI
import UserNotifications
#main
struct CustomWatchNotificationsApp: App
{
let notifications = NotificationController()
var body: some Scene
{
WindowGroup
{
ContentView(notifications: notifications)
}
}
}
class NotificationController
{
func requestPermissions()
{
Task {
try await UNUserNotificationCenter.current()
.requestAuthorization(
options: [.alert, .sound])
}
}
func scheduleNotification()
{
let content = UNMutableNotificationContent()
content.title = "Take Action!"
content.categoryIdentifier = "takeActionCategory"
content.sound = .default
// Schedule a new notification 5 seconds from now,
// so there is enough time to lock the phone screen
// to deliver notification to Apple Watch.
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: 5,
repeats: false)
let request = UNNotificationRequest(
identifier: "takeAction",
content: content,
trigger: trigger)
UNUserNotificationCenter.current()
.add(request)
}
}
This is the ContentView for the iOS app, which just includes the two buttons:
import SwiftUI
struct ContentView: View
{
let notifications: NotificationController
var body: some View
{
NavigationView
{
Form
{
// Request notification permissions
Button
{
notifications.requestPermissions()
} label: {
Text("Request Notification Permissions")
}
// Schedule notification
Button
{
notifications.scheduleNotification()
} label: {
Text("Schedule Notification")
}
}
}
}
}
On the watchOS side, I updated the main app file to include a custom View for the notification, inside a WKUserNotificationHostingController for this specific notification category:
import SwiftUI
import UserNotifications
#main
struct CustomWatchNotifications_Watch_AppApp: App
{
var body: some Scene
{
WindowGroup
{
ContentView()
}
WKNotificationScene(
controller: TakeActionNotificationController.self,
category: "takeActionCategory"
)
}
}
struct TakeActionNotificationView: View
{
var body: some View
{
Text("This is a test.")
}
}
class TakeActionNotificationController:
WKUserNotificationHostingController<TakeActionNotificationView>
{
// This does not seem to have an effect on sashColor.
override class var sashColor: Color?
{
return .red
}
override var body: TakeActionNotificationView
{
return TakeActionNotificationView()
}
// This has to be here for custom notification to show up.
override func didReceive(_ notification: UNNotification)
{}
}
When you build and run on real devices, make sure the watchOS app is installed before schedule the test notification. Once you schedule the test notification, lock the iPhone screen immediately so the notification gets delivered to Apple Watch.
I have problem with making sidebar look transparent. All advices about implementing it (even official Apple Documentation) is on AppDelegate/SceneDelegate. But since last update projects have only this file:
'''
import SwiftUI
import UIKit
#main
struct SpenTApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
SideBarView()
.background(Color.clear)
DetailView()
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
}
Catalyst apps have some weird limitations, so to do what you want: SideBarView must be a List with listStyle(SidebarListStyle()).
I'm trying to execute some code I'd have previously put in my app delegate, such as saving my managed object context when entering the background. I put the call in the .onChange for the scenePhase, but I'm not getting anything. Here's a sample project:
import SwiftUI
#main
struct PhaseApp: App {
#Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
Text("Hello, world.")
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
print("Active")
case .background:
print("Background")
case .inactive:
print("Inactive")
#unknown default: break
}
}
}
}
I'd expect to get a print command in the Simulator or on my test device whenever I press Home or tap the app, but nothing happens.
I acknowledge this question is specifically about schenePhase changes, however, on macOS I am not able to receive any .background notifications when a user switches to a different app. The older NotificationCenter strategy works as I expected, on both platforms. I'll add this to the mix for anyone who is just trying to execute some code, onForeground / onBackground on iOS and macOS.
On any view, you can attach:
.onReceive(NotificationCenter.default.publisher(for: .willResignActiveNotification)) { _ in
doBackgroundThing()
}
The events you may care about are:
iOS: willResignActiveNotification & willEnterForegroundNotification
macOS: willResignActiveNotification & willBecomeActiveNotification
You can find all NotificationCenter Names here.
I use will* variants for background because I assume they'll be called early in the process, and I use did* variants for foreground, because they are called regardless of whether the app is launched for the first time, or it's coming out of background.
I use this extension so I don't have to think about the platform differences:
extension View {
#if os(iOS)
func onBackground(_ f: #escaping () -> Void) -> some View {
self.onReceive(
NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification),
perform: { _ in f() }
)
}
func onForeground(_ f: #escaping () -> Void) -> some View {
self.onReceive(
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification),
perform: { _ in f() }
)
}
#else
func onBackground(_ f: #escaping () -> Void) -> some View {
self.onReceive(
NotificationCenter.default.publisher(for: NSApplication.willResignActiveNotification),
perform: { _ in f() }
)
}
func onForeground(_ f: #escaping () -> Void) -> some View {
self.onReceive(
NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification),
perform: { _ in f() }
)
}
#endif
}
As expected, I use it as such:
AppView()
.onBackground {
print("my background")
}
.onForeground {
print("my foreground")
}
Use inside scene root view (usually ContentView)
Tested with Xcode 12 / iOS 14 as worked.
struct ContentView: View {
#Environment(\.scenePhase) private var scenePhase
var body: some View {
TestView()
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
print(">> your code is here on scene become active")
case .inactive:
print(">> your code is here on scene become inactive")
case .background:
print(">> your code is here on scene go background")
default:
print(">> do something else in future")
}
}
}
}
I've been testing with Xcode 12 beta 3 and iOS/iPadOS 14 beta 3 and here's what I'm finding. Note that a lot of this involves supporting multiple windows, but the "SwiftUI lifecycle" projects default to turning that on, so I suspect you have it active already. In my original case I was porting an existing SwiftUI app from a SceneDelegate to using the new App struct, so I had multiple window support already active.
Here's the test View I'm using in a new testing app:
struct ContentView: View {
#Environment(\.scenePhase) private var scenePhase
var body: some View {
Text("Hello, world!").padding()
.onChange(of: scenePhase) { phase in
switch phase {
case .background:
print("PHASECHANGE: View entered background")
case .active:
print("PHASECHANGE: View entered active")
case .inactive:
print("PHASECHANGE: View entered inactive")
#unknown default:
print("PHASECHANGE: View entered unknown phase.")
}
}
}
}
(I have identical code in the App & Scene but they never print anything.)
The ScenePhase documentation claims that you can declare onChange inside the App, a Scene or a View. I don't see the App or Scene level versions ever execute, under any circumstance I can engineer, and the View level versions don't seem to execute completely correctly.
On hardware that doesn't support multiple windows (I use a 7th generation iPod touch) the View level closure executes every time. (Full disclosure, this iPod Touch is still running beta 2, but I don't think it's going to matter. Once I update it to b3 I'll mention it here if it matters.) EDIT (It did matter.)
On hardware running beta 2 that doesn't support multiple windows (a 7th generation iPod Touch) I see the app go into the background, back into the foreground, and so forth. On every app launch I'll see "View entered active" print.
On hardware that does support multiple windows (I use an older iPad Pro with the Lightning connector) I don't see the initial scene creation happen. (The first run does not trigger a "View entered active" message.) I do see subsequent background/foreground transitions. If I create a new scene from the iPad multi-tasking UI the second scene will trigger a "View entered active" log. Unfortunately I hadn't run this test on the iPad against beta 2, so I can't say if the behavior changed with b3 or not.
On my iPod Touch running iOS 14 beta 3 I see the same behavior as the iPad: the first launch doesn't print any phase change messages from the view, but does report subsequent background/foreground changes.
On the simulator it always behaves like the iPad hardware, even when I'm simulating an iPod Touch. I suspect this is because the simulator is running under the hood on the Mac and gets multiple window "support" this way. But I do see messages when I put the app in the background while running in the simulator, I'm just missing the initial "View entered active" message that I get from the actual hardware.
One final note: when I return an app from the foreground I first see "View entered inactive" and then I see "View entered active". When I background the app I see "View entered inactive", followed by "View entered background". I think this is expected behavior, but since other parts seem broken I wanted to mention it.
TL;DR:
I think you should be able to see most ScenePhase changes from a View, but you'll miss the initial app launch on iPads or in the simulator. And hopefully they will show up as expected for App and Scene objects in a later beta?
You can use the following extension:
public extension View {
func onScenePhaseChange(phase: ScenePhase, action: #escaping () -> ()) -> some View {
self.modifier(OnScenePhaseChangeModifier(phase: phase, action: action))
}
}
public struct OnScenePhaseChangeModifier: ViewModifier {
#Environment(\.scenePhase) private var scenePhase
public let phase: ScenePhase
public let action: () -> ()
public func body(content: Content) -> some View {
content
.onChange(of: scenePhase) { phase in
if (self.phase == phase) {
action()
}
}
}
}
Final usage:
ContentView()
.onScenePhaseChange(phase: .active) { print("scene activated!") }
.onScenePhaseChange(phase: .background) { print("scene backgrounded!") }
.onScenePhaseChange(phase: .inactive) { print("scene inactive!") }
In my case, I put "#Environment(.scenePhase) private var scenePhase" in ContentView. Then the onChange works in the child views.
The PresentationButton presents the view properly, but upon hitting "Cancel" to return to parent view, the button refuses to present the child view again.
Here is my PresentationButton code:
struct ContentView : View {
var body: some View {
ZStack {
PresentationButton(Text("Click to show"), destination: SomeOtherView())
.transition(.slide)
}
}
}
This bug has existed in various forms until Xcode 11 beta 4, but note that the API has changed.
PresentationButton became PresentationLink and then was deprecated in Xcode 11 beta 4 in favor of .sheet.
See here for an example of how to use the new functionality: https://stackoverflow.com/a/57087399/3179416