I have below logic which give app state:
But i wanted to add logic when app move to foreground.
In Swift we have method. I am looking for something similar method in swiftui
func sceneWillEnterForeground(_ scene: UIScene) {
but in Swiftui i only have .active, .inactive and .background
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
} else if newPhase == .inactive {
} else if newPhase == .background {
}
}
It is possible to use .onReceive with subscription to Notification Center publisher on UIScene.willEnterForegroundNotification notification, or, after all, mentioned scene delegate callback (having scene delegate as described in https://stackoverflow.com/a/60359809/12299030)
Related
Environment: SwiftUI using Swift 5.3
Scenario: The default orientation is Portrait, LandscapeLeft & LandscapeRight per Xcode General Setting. This allows the possibility to have landscape on demand versus having the Xcode Setting to Portrait only. The project is using SwiftUI Lifecycle vs AppDelegate.
Goal: To have ONLY particular Views able to rotate to landscape; the majority locked in portrait.
Current Modus Operandi: The device is set for Portrait-Only mode within the current View's .upAppear{} and via onReceive{} via the device Orientation-Change Notification.
I found this the only way to actually do a momentary Portrait lock, allowing others to render for landscape.
Problem: The Orientation-Change Notification happens TOO LATE: I see the actual landscape being corrected in real time - so the image snaps back during the rotate.
Question: How to I lock a specific swiftUI View in Portrait mode, allowing others to freely change orientation?
import SwiftUI
import UIKit
struct ContentView: View {
var body: some View {
ZStack {
Color.blue
NavigationView {
Text("Hello, world!")
.padding()
.navigationTitle("Turkey Gizzards")
.navigationBarTitleDisplayMode(.inline)
}
}.onAppear {
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
}.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
}
}
}
There is no native SwiftUI method for doing that. It looks like for now, it is mandatory to use the AppDelegate adaptor.
Inside your App main, add this:
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
Then add this class:
class AppDelegate: NSObject, UIApplicationDelegate {
static var orientationLock = UIInterfaceOrientationMask.all //By default you want all your views to rotate freely
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return AppDelegate.orientationLock
}
}
And in the specific view designed to be locked in portrait mode:
struct ContentView: View {
var body: some View {
ZStack {
Color.blue
NavigationView {
Text("Hello, world!")
.padding()
.navigationTitle("Turkey Gizzards")
.navigationBarTitleDisplayMode(.inline)
}
}.onAppear {
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") // Forcing the rotation to portrait
AppDelegate.orientationLock = .portrait // And making sure it stays that way
}.onDisappear {
AppDelegate.orientationLock = .all // Unlocking the rotation when leaving the view
}
}
}
You may also, depending on your needs, add another UIDevice.current.setValue(UIInterfaceOrientation.yourOrientation.rawValue, forKey: "orientation") inside the onDisappear to force the rotation when leaving the view.
Are you using UIHostingController? Another workaround might be subclassing it to implement supportedInterfaceOrientations / shouldAutorotate as we might normally do in UIKit:
class HostingController<Content>: UIHostingController<Content> where Content: View {}
The provided solutions were not working on iOS 16 and I cleaned up the SwiftUI implementation.
My solution rotates to the correct orientation and locks the rotation. The default is portrait and forces landscape on specific views. This can be changed to your needs.
In the AppDelegate add:
static var orientationLock = UIInterfaceOrientationMask.portrait {
didSet {
if #available(iOS 16.0, *) {
UIApplication.shared.connectedScenes.forEach { scene in
if let windowScene = scene as? UIWindowScene {
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationLock))
}
}
UIViewController.attemptRotationToDeviceOrientation()
} else {
if orientationLock == .landscape {
UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation")
} else {
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
}
}
}
}
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return AppDelegate.orientationLock
}
Create a View+Extensions and add the following code:
extension View {
#ViewBuilder
func forceRotation(orientation: UIInterfaceOrientationMask) -> some View {
self.onAppear() {
AppDelegate.orientationLock = orientation
}
// Reset orientation to previous setting
let currentOrientation = AppDelegate.orientationLock
self.onDisappear() {
AppDelegate.orientationLock = currentOrientation
}
}
}
Then in SwiftUI you can do:
struct DummyView: View {
var body: some View {
ZStack {
Text("Dummy View")
}.forceRotation(orientation: .landscape)
}
}
Partway through the 2020 Apple platform API betas, the method mentioned in the subject was added. (A similar method was added to AnyView.) Does anyone know where the corresponding API to send the external events in the first place is?
Sample for open new window in macOS using sending of Scene.handlesExternalEvents
#main
struct TestAppApp: App {
var body: some Scene {
WindowGroup {
MainView()
}
//subscribe on event of open mainView
.handlesExternalEvents(matching: Set(arrayLiteral: Wnd.mainView.rawValue))
WindowGroup {
HelperView()
}
//subscribe on event of open helperView
.handlesExternalEvents(matching: Set(arrayLiteral: Wnd.helperView.rawValue))
}
}
enum Wnd: String, CaseIterable {
case mainView = "MainView"
case helperView = "OtherView"
func open(){
if let url = URL(string: "taotao://\(self.rawValue)") {
print("opening \(self.rawValue)")
NSWorkspace.shared.open(url)
}
}
}
and 2 Windows/Views code:
extension TestAppApp {
struct MainView: View {
var body: some View {
VStack {
Button("Open Main View") {
Wnd.mainView.open()
}
Button("Open Other View") {
Wnd.helperView.open()
}
}
.padding(150)
}
}
struct HelperView: View {
var body: some View {
HStack {
Text("This is ") + Text("Helper View!").bold()
}
.padding(150)
}
}
}
Info.plist changes also needed :
My understanding is that this modifier which works on WindowGroups or Views is there to indicate that the Scene or View supports NSUserActivity which are sent by Handoff, Spotlight, SiriKit or Universal Links. In your Info.plist you declare activities that your app supports (as String identifiers) and in your App structure you indicate which Scene handles which activity so the frameworks can decide the proper Scene to open. In other words, it is not for sending NSEvents within different objects of your app but rather to let SwiftUI know which View or Scene can handle a user activity that your app advertises.
I want to create a POC using SwiftUI and CoreML. I use a simple button to call some function (called test here). This function is pretty heavy as it performs a CoreML inference, and it can take up to a minute to compute.
I have several problems:
The button is still active even when the computation is ongoing. In other words, if I click the button several times before the processing of the first click is finished, the processing will be performed several times. I want to disable the button as long as the processing is ongoing.
I tried to modify the button's appearance to signify the user that the processing is ongoing. In the example bellow, I change the button color to red before calling the test function, and I change it back to blue when the processing is over. But it doesn't work.
In the code bellow, the test function is just sleeping for 5 seconds to simulate the CoreML inference.
func test() -> Void {
print("Start processing")
sleep(5)
print("End processing")
}
struct ContentView: View {
#State private var buttonColor : Color = .blue
var body: some View {
VStack {
Button(action: {
self.buttonColor = .red
test()
self.buttonColor = .blue
}) {
Text("Start")
.font(.title)
.padding(.horizontal, 40)
.padding(.vertical, 5)
.background(self.buttonColor)
.foregroundColor(.white)
}
}
}
}
I guess this problem is very straight forward for most of you. I just can't find the correct search keywords to solve it by myself. Thanks for your help.
Here is possible approach (see also comments in code). Tested & works with Xcode 11.2 / iOS 13.2.
struct ContentView: View {
#State private var buttonColor : Color = .blue
var body: some View {
VStack {
Button(action: {
self.buttonColor = .red
DispatchQueue.global(qos: .background).async { // do in background
test()
DispatchQueue.main.async {
self.buttonColor = .blue // work with UI only in main
}
}
}) {
Text("Start")
.font(.title)
.padding(.horizontal, 40)
.padding(.vertical, 5)
.background(self.buttonColor)
.foregroundColor(.white)
}
.disabled(self.buttonColor == .red) // disabled while calculating
}
}
}
SwiftUI MagnificationGesture and DragGesture have .onChanged and .onEnded APIs but nothing to check when the gesture started like in UIKit. Couple ways I thought to do it:
.$gestureStarted bool in onChanged and then set it back to false in .onEnded
use a gesture sequence with tap.
Am I missing some preferred way to do this? Seems like a pretty natural thing to want to check.
There is special #GestureState, which can be used for such purpose. So, here is possible approach
struct TestGestureBegin: View {
enum Progress {
case inactive
case started
case changed
}
#GestureState private var gestureState: Progress = .inactive // initial & reset value
var body: some View {
VStack {
Text("Drag over me!")
}
.frame(width: 200, height: 200)
.background(Color.yellow)
.gesture(DragGesture(minimumDistance: 0)
.updating($gestureState, body: { (value, state, transaction) in
switch state {
case .inactive:
state = .started
print("> started")
case .started:
state = .changed
print(">> just changed")
case .changed:
print(">>> changing")
}
})
.onEnded { value in
print("x ended")
}
)
}
}
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
// ...