I'm currently developing an application using SwiftUI.
I'm trying to make a timer app and I want to control Timer.publish() method when the app in the background.
In my App, I need to continue to work Timer.publish() to count up a number when the App in active and background So I made a setting like below:
Open the project file
Add the capability of the Background Modes
Check Audio, Airplay, and Picture in Picture as the image I attached
I want to continue to work Timer.publish() in the background only when I set a running as timerStatus , but in the case of my codes, even when I check stoped, Timer.publish() work in background.
How could I work Timer.publish() in the background only when I check running as timerStatus?
(I want to disappear onRecieve by print method when the app in the background and set stoped as timerStatus )
Here are the codes:
ContentView.swift
import SwiftUI
struct ContentView: View {
#Environment(\.scenePhase) private var scenePhase
var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var timerStop = Timer()
#State var appStatus: AppStatus = .active
#State var timerStatus: TimerStatus = .running
#State var counter = 0
var body: some View {
VStack{
HStack{
Text("RUN")
.onTapGesture {
timerStatus = .running
}
Text("STOP")
.onTapGesture {
timerStatus = .stoped
}
}
Text(timerStatus == .running ? "running" : "stoped")
.padding()
Text(String(counter))
.padding()
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
appStatus = .active
case .inactive:
appStatus = .inactive
case .background:
appStatus = .background
default:
break
}
}
.onReceive(timer) { _ in
print("onRecieve")
if timerStatus == .running{
counter += 1
}
}
}
}
enum AppStatus {
case active
case inactive
case background
}
enum TimerStatus {
case running
case stoped
}
TimerControlApp.swift
import SwiftUI
#main
struct TimerControlApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Xcode: Version 12.0.1
iOS: 14.0
Life Cycle: SwiftUI App
Related
I'm implementing a Timer, to show an Alert after 10 seconds, using Combine's Publisher and #ObservedObject, #StateObject or #State to manage states in a screen A. The problem is when I navigate to screen B through a NavigationLink the Alert still show up.
Is there a way to process the state changes of a view only when it's on top?
struct NavigationView: View {
let timer = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.receive(on: DispatchQueue .main)
.scan(0) { counter, _ in
counter + 1
}
#State private var counter = "Seconds"
#State private var alert: AlertConfiguration?
var body: some View {
ZStack {
HStack(alignment: .top) {
Text(counterText)
Spacer()
}
NavigationLink(
destination: destinationView
) {
Button(Strings.globalDetails1) {
navigationAction()
}
}
}
.onReceive(timer) { count in
if count == 10 {
makeAlert()
}
setSeconds(with: count)
}
.setAlert(with: $alert) // This is just a custom ViewModifier to add an Alert to a view
}
private func makeAlert() {
alert = AlertConfiguration()
}
private func setSeconds(with count: Int) {
counter = "seconds_counter".pluralLocalization(count: count)
}
}
You could add a variable that you set true if your View is in foreground and will be set false if you switch to a different View. Then you only increase the timer if said variable is true
.onReceive(timer) {
if count == 10 {
makeAlert()
}
if (timerIsRunning) {
count += 1
}
}
So basically the solution is to have a publisher that supports the lifecycle of the screen (onAppear & onDisappear).
This Answer helped: SwiftUI Navigation Stack pops back on ObservableObject update
I am starting out in SwiftUI and have an issue.
I have a main view loads a modal view, on iPhone this goes full screen, iPad by default covers part of the screen.
The below code appears to do the 'default' loads a view that is centered but not full screen.
What id ideally like is to be able to make the model view smaller. It is a login screen where user enters login details.
Using storyboards, I could achieve this with 'preferredcontentsize' but SwiftUI comparisons don't appear to work.
struct Login_View: View {
#State private var showingSheet = false
var body: some View {
Button("Show Alert") {
showingSheet = !showingSheet
}
.sheet(isPresented:$showingSheet) {
Credentials_View()
}
}
}
(Below is the modal view, atm it just shows some colours whilst I get to grips with that is going on)
struct Credentials_View: View {
var body: some View {
GeometryReader { metrics in
VStack(spacing: 0) {
Color.red.frame(height: metrics.size.height * 0.43)
Color.green.frame(height: metrics.size.height * 0.37)
Color.yellow
}
}
}
}
IOS 16+
As of iOS 16, a half sheet can be created using .sheet where the displayed view(example Credentials_View) has the .presentationDetents set to .medium.
struct Login_View: View {
#State private var showingSheet = false
var body: some View {
Button("Show Alert") {
showingSheet = !showingSheet
}
.sheet(isPresented:$showingSheet) {
Credentials_View()
.presentationDetents([.medium])
}
}
}
Result:
And .presentationDragIndicator can be added to make the indicator visible:
.presentationDragIndicator(.visible)
Result:
I'm currently developing an application using SwiftUI.
I'm trying to make an app using Local Notification.
I want to reflect on a set of Allow Notifications in an App Settings to some views.
But when a view comes back from the App Settings using a navigation link I attached(not back button), the onAppear method doesn't fire and I can't show a collect value...
In my codes:
1.Tap Open Settings to navigate to an App Setting
2.Change a Notifications setting
3.Tap NotificationTest button
4.Come back to ContentView then onAppear method doesn't fire
How could I solve this problem?
Here are the codes:
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var isNotification = false
var body: some View {
VStack{
Text(isNotification ? "STATUS:ON" : "STATUS:OFF")
.padding()
Text("Open Settings")
.padding()
.onTapGesture {
UIApplication.shared.open(URL(string:UIApplication.openSettingsURLString)!)
}
}
.onAppear(){
confirmNotification()
}
}
func confirmNotification(){
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .badge, .sound]){
success, error in
if success{
isNotification = true
print("Notification set")
}else{
isNotification = false
}
}
}
}
NotificationTestApp.swift
import SwiftUI
#main
struct NotificationTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Xcode: Version 12.0.1
iOS: 14.0
Life Cycle: SwiftUI App
View has already appeared, just application is in background. Try to use instead (or additionally) scenePhase
struct ContentView: View {
#Environment(\.scenePhase) private var scenePhase
#State var isNotification = false
var body: some View {
VStack{
Text(isNotification ? "STATUS:ON" : "STATUS:OFF")
.padding()
Text("Open Settings")
.padding()
.onTapGesture {
UIApplication.shared.open(URL(string:UIApplication.openSettingsURLString)!)
}
}
.onAppear(){
confirmNotification()
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
confirmNotification()
default:
break
}
}
}
I am trying to use the new App protocol for a new SwiftUI App and I need to detect a scenePhase change into .background at App level in order to persist little App data into a .plist file. I don't know if it is a bug or I am doing something wrong but it doesn't work as expected. As soon as a button is tapped, scenePhase change to .background when the scene is still active! In order to show an example of this weird behaviour, I am showing this simple code:
class DataModel: ObservableObject {
#Published var count = 0
}
#main
struct TestAppProtocolApp: App {
#Environment(\.scenePhase) private var scenePhase
#StateObject private var model: DataModel = DataModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .active:
print("Scene is active.")
case .inactive:
print("Scene is inactive.")
case .background:
print("Scene is in the background.")
#unknown default:
print("Scene is in an unknown state.")
}
}
}
}
struct ContentView: View {
#EnvironmentObject var model: DataModel
var body: some View {
VStack {
Button(action: { model.count += 1 }) {
Text("Increment")
}
.padding()
Text("\(model.count)")
}
}
}
When the increment button is tapped, scenePhase changes to .background and then, when the App is really sent to background, scenePhase is not changed.
I found out that moving the .onChange(of: scenePhase) to the View (ContentView) works fine as I expect but Apple announced you can monitor any scenePhase change at App level and this is what I really want not at View level.
I also had a similar issue with scenePhase not working at all, then it worked but not as expected. Try removing only "#StateObject private" from your property and you will probably have other results. I hope new betas will fix this.
By the way, the recommended way of persisting little App-wide data in SwiftUI 2+ is through #AppStorage property wrapper which itself rests on UserDefaults. Here is how we can detect the first launch and toggle the flag:
struct MainView: View {
#AppStorage("isFirstLaunch") var isFirstLaunch: Bool = true
var body: some View {
Text("Hello, world!")
.sheet(isPresented: $isFirstLaunch, onDismiss: { isFirstLaunch = false }) {
Text("This is the first time app is launched. The sheet will not be shown again once dismissed.")
}
}
}
Xcode12 beta 6 seems to solve the issue. Now it works as expected.
I wanna create a button with SwiftUI that fires the moment my finger touches it (like UIKit's touch down instead of touch up inside). I also want the opacity of the button to become 0.7 when my finger is pressing the button. And I want the opacity of the button to change back to 1 ONLY when my finger is no longer touching the button.
I've tried 2 different types of button styles to create such a button but both of them failed:
struct ContentView: View {
var body: some View {
Button(action: {
print("action triggered")
}){
Text("Button").padding()
}
.buttonStyle(SomeButtonStyle())
}
}
struct SomeButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(Color.green)
.opacity(configuration.isPressed ? 0.7 : 1)
.onLongPressGesture(
minimumDuration: 0,
perform: configuration.trigger//Value of type 'SomeButtonStyle.Configuration' (aka 'ButtonStyleConfiguration') has no member 'trigger'
)
}
}
struct SomePrimativeButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(Color.green)
.opacity(configuration.isPressed ? 0.7 : 1)//Value of type 'SomePrimativeButtonStyle.Configuration' (aka 'PrimitiveButtonStyleConfiguration') has no member 'isPressed'
.onLongPressGesture(
minimumDuration: 0,
perform: configuration.trigger
)
}
}
Apparently none of the button styles above worked because ButtonStyle and PrimitiveButtonStyle don't share the same methods and properties so I can't use both the isPressed property (which belongs to ButtonStyle) AND the trigger method (which belongs to PrimitiveButtonStyle) in the same button style.
How should I configure my button style to make this work?
Ok, I understand that author wants to see solution only with Button, so I dig a little more. And found something interesting at Swift UI Lab. The idea is the same as in my first answer: use #GestureState and create LongPressGesture which .updating($...) this state. But in PrimitiveButtonStyle you don't need to compose a few gestures together. So, I simplified code a little and tested it at simulator. And I think now it just what author need:
struct ComposingGestures: View {
var body: some View {
Button(action: {
print("action triggered")
}){
Text("Button")
.padding()
}
.buttonStyle(MyPrimitiveButtonStyle())
}
}
struct MyPrimitiveButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View {
MyButton(configuration: configuration)
}
struct MyButton: View {
#GestureState private var pressed = false
let configuration: PrimitiveButtonStyle.Configuration
let color: Color = .green
#State private var didTriggered = false
var body: some View {
// you can set minimumDuration to Double.greatestFiniteMagnitude if you think that
// user can hold button for such a long time
let longPress = LongPressGesture(minimumDuration: 300, maximumDistance: 300.0)
.updating($pressed) { value, state, _ in
state = value
self.configuration.trigger()
}
return configuration.label
.background(Color.green)
.opacity(pressed ? 0.5 : 1.0)
.gesture(longPress)
}
}
}
I didn't work with ButtonStyle, but tried to solve it with Composing SwiftUI Gestures. I compose TapGesture and LongPressGesture and playing with #GestureState to control .opacity of "button" (which is just Text). The result is just as you asked:
struct ComposingGestures: View {
enum TapAndLongPress {
case inactive
case pressing
var isPressing: Bool {
return self == .pressing
}
}
#GestureState var gestureState = TapAndLongPress.inactive
#State private var didPress = false
var body: some View {
let tapAndLongPressGesture = LongPressGesture(minimumDuration: 2) // if minimumDuration <= 1 gesture state returns to inactive in 1 second
.sequenced(before: TapGesture())
.updating($gestureState) { value, state, transaction in
switch value {
case .first(true), .second(true, nil):
self.didPress = true // simulation of firing action
state = .pressing
default:
state = .pressing
}
}
return VStack {
Text("action was fired!")
.opacity(didPress ? 1 : 0)
Text("Hello world!")
.gesture(tapAndLongPressGesture)
.background(Color.green)
.opacity(gestureState.isPressing ? 0.7 : 1)
}
}
}
P.S. I played only with #State var didPress to show, how to fire action. Maybe it's better to fire it only in the first case, like this:
// ...
.updating($gestureState) { value, state, transaction in
switch value {
case .first(true):
self.didPress = true // simulation of firing action
state = .pressing
case .second(true, nil):
state = .pressing
default:
state = .pressing
}
UPDATE
tried code at simulator and fixed two mistakes:
// ...
let tapAndLongPressGesture = LongPressGesture(minimumDuration: 300, maximumDistance: 300) // now button don't return opacity to 1 even if you move your finger
// ...
case .first(true), .second(true, nil):
DispatchQueue.main.async { // now there are no purple mistakes
self.didPress = true // simulation of firing action
}
state = .pressing
// ...