Why updating #State inside AppDelegate does not work? - swiftui

import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#State var isFocused: Bool = true
var window: NSWindow!
// Some window setup code here
func applicationDidBecomeActive(_ notification: Notification) {
isFocused = true
}
func applicationDidResignActive(_ notification: Notification) {
isFocused = false
}
}
I tried to print isFocused value and it is always true even immediately after isFocused = false statement. Is #State only working inside SwiftUI Views?

Documentation:
/// A linked View property that instantiates a persistent state
/// value of type `Value`, allowing the view to read and update its
/// value.
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
#frozen #propertyWrapper public struct State<Value> : DynamicProperty {
So, State is for SwiftUI View, but AppDelegate is-not-a View.

Related

How to undo by code a SwiftUI swipe action on a List cell?

My app has a SwiftUI List with cells that can be swiped (leading, trailing).
Problem:
Say, a cell is swiped so that the buttons are visible. Then the app is de-activated in this state, e.g. by a switch to another app, by locking the screen, etc.
When the app later is re-activated, the earlier swiped cell is still swiped, although the user might no longer be aware of the reason.
It would thus be better to undo the swipe by code, when the app is deactivated.
Question:
Is this possible?
It is a but of a brute-force approach but redrawing with this approach works.
import SwiftUI
#available(iOS 15.0, *)
struct ResetSwipeView: View {
#Environment(\.scenePhase) var scenePhase
#State var id: UUID = .init()
var body: some View {
List(1...10){n in
Text(n, format: .number)
.swipeActions {
Button {
print("Button :: \(n)")
} label: {
Text("print")
}
}
}.id(id)
.onChange(of: scenePhase, perform: { newValue in
if newValue == .inactive{
id = .init()
}
})
}
}
#available(iOS 15.0, *)
struct ResetSwipeView_Previews: PreviewProvider {
static var previews: some View {
ResetSwipeView()
}
}

Is it possible to reload only modified item in SwiftUI List/ForEach that uses #Binding

Simple sample code with toggle button (slightly modified from hackingwithswift:
This code(hackingwithswift original and my version) IS redrawing every list cell whenever any toggle happens. I modified code to better debug view drawing.
import SwiftUI
struct User: Identifiable {
let id = UUID()
var name: String
var isContacted = false
}
struct ProfileView: View {
#State private var users = [
User(name: "Taylor"),
User(name: "Justin"),
User(name: "Adele")
]
var body: some View {
let _ = Self._printChanges()
List($users) { $user in
ProfileCell(user: $user)
}
}
}
struct ProfileCell: View{
#Binding var user: User
var body: some View{
let _ = Self._printChanges()
Text(user.name)
Spacer()
Toggle("User has been contacted", isOn: $user.isContacted)
.labelsHidden()
}
}
Running app and toggling will print following in console for every toggle:
ProfileView: _users changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
Hackingwithswift tutorial states "Using a binding in this way is the most efficient way of modifying the list, because it won’t cause the entire view to reload when only a single item changes.", however that does not seem to be true.
Is it possible to redraw only item that was changed?
Theoretically it should be working, but it seems they changed something since first introduction, because now on state change they recreate(!) bindings (all of them), so automatic view changes handler interpret that as view update (binding is a property after all).
A possible workaround for this is to help rendering engine and check view equitability manually.
Tested with Xcode 13.4 / iOS 15.5
Main parts:
// 1
List($users) { $user in
EquatableView(content: ProfileCell(user: $user)) // << here !!
}
// 2
struct ProfileCell: View, Equatable {
static func == (lhs: ProfileCell, rhs: ProfileCell) -> Bool {
lhs.user == rhs.user
}
// ...
// 3
struct User: Identifiable, Equatable {
Test module is here

Equatable object not updated when content updated

I'm trying to wrap my head around how SwiftUI deals with Equatable, and I can't understand why onChange doesn't seem to trigger when an equatable object is updated.
Below is an example of what I'm confused about. If you use the slider, the text updates just fine, but the OnChange doesn't trigger. It only triggers if you change the number of objects in the Array.
PointAnnotation is equatable by default, but to double-check it's comparing latitude I've also made it adhere to equatable manually. Doing so doesn't change the behavior.
What am I missing?
import SwiftUI
struct ContentView: View {
#ObservedObject var controller = Controller()
#State private var change:Bool = false
var body: some View {
VStack {
Text("Obj 0 lat: \(controller.object.annotations[0].coordinate.latitude)")
.padding()
Text("Obj changed: \(change ? "true" : "false")")
Slider(value: $controller.object.annotations[0].coordinate.latitude, in: (0...10))
}
.onChange(of: controller.object, perform: { value in change = true })
}
}
class Controller:ObservableObject {
#Published var object = Object()
}
struct Object:Equatable {
var annotations:[MapAnnotation] = MapAnnotation.exampleArray
}
import MapKit
class MapAnnotation:MKPointAnnotation {
var id:String = UUID().uuidString
}
extension MapAnnotation {
static var exampleArray:[MapAnnotation] = [ MapAnnotation(), MapAnnotation(), MapAnnotation() ]
}

SwiftUI: How to properly present AVPlayerViewController modally?

Proper UIKit Approach:
According to Apple's WWDC 2019 talk on the subject, AVPlayerViewController should be presented modally to take advantage of all the latest full-screen features of the API. This is the recommended sample code to be called from your presenting UIKit view controller:
// Create the player
let player = AVPlayer(url: videoURL)
// Create the player view controller and associate the player
let playerViewController = AVPlayerViewController()
playerViewController.player = player
// Present the player view controller modally
present(playerViewController, animated: true)
This works as expected and launches the video in beautiful full-screen.
Achieve the Same Effect with SwiftUI?:
In order to use the AVPlayerViewController from SwiftUI, I created the UIViewControllerRepresentable implementation:
struct AVPlayerView: UIViewControllerRepresentable {
#Binding var videoURL: URL
private var player: AVPlayer {
return AVPlayer(url: videoURL)
}
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
playerController.player = player
playerController.player?.play()
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
return AVPlayerViewController()
}
}
I cannot seem to figure out how to present this directly from SwiftUI
in the same way as the AVPlayerViewController is presented directly
from UIKit. My goal is simply to get all of the default, full-screen benefits.
So far, the following has not worked:
If I use a .sheet modifier and present it from within the sheet, then the player is embedded in a sheet and not presented full-screen.
I have also tried to create a custom, empty view controller in UIKit that simply presents my AVPlayerViewController modally from the viewDidAppear method. This gets the player to take on the full screen, but it also shows an empty view controller prior to display the video, which I do not want the user to see.
Any thoughts would be much appreciated!
Just a thought if you like to fullscreen similar like UIKit, did you try the following code from ContentView.
import SwiftUI
import UIKit
import AVKit
struct ContentView: View {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
#State private var vURL = URL(string: "https://www.radiantmediaplayer.com/media/bbb-360p.mp4")
var body: some View {
AVPlayerView(videoURL: self.$vURL).transition(.move(edge: .bottom)).edgesIgnoringSafeArea(.all)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AVPlayerView: UIViewControllerRepresentable {
#Binding var videoURL: URL?
private var player: AVPlayer {
return AVPlayer(url: videoURL!)
}
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
playerController.modalPresentationStyle = .fullScreen
playerController.player = player
playerController.player?.play()
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
return AVPlayerViewController()
}
}
The solution explained by Razib-Mollick was a good start for me, but it was missing the use of the SwiftUI .sheet() method. I have added this by adding the following to ContentView:
#State private var showVideoPlayer = false
var body: some View {
Button(action: { self.showVideoPlayer = true }) {
Text("Start video")
}
.sheet(isPresented: $showVideoPlayer) {
AVPlayerView(videoURL: self.$vURL)
.edgesIgnoringSafeArea(.all)
}
}
But the problem is then, that the AVPlayer is instantiated again and again when SwiftUI re-renders the UI.
Therefore the state of the AVPlayer has to move to a class object stored in the environment, so we can get hold of it from the View struct. So my latest solution looks now as follows. I hope it helps somebody else.
class PlayerState: ObservableObject {
public var currentPlayer: AVPlayer?
private var videoUrl : URL?
public func player(for url: URL) -> AVPlayer {
if let player = currentPlayer, url == videoUrl {
return player
}
currentPlayer = AVPlayer(url: url)
videoUrl = url
return currentPlayer!
}
}
struct ContentView: View {
#EnvironmentObject var playerState : PlayerState
#State private var vURL = URL(string: "https://www.radiantmediaplayer.com/media/bbb-360p.mp4")
#State private var showVideoPlayer = false
var body: some View {
Button(action: { self.showVideoPlayer = true }) {
Text("Start video")
}
.sheet(isPresented: $showVideoPlayer, onDismiss: { self.playerState.currentPlayer?.pause() }) {
AVPlayerView(videoURL: self.$vURL)
.edgesIgnoringSafeArea(.all)
.environmentObject(self.playerState)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(PlayerState())
}
}
struct AVPlayerView: UIViewControllerRepresentable {
#EnvironmentObject var playerState : PlayerState
#Binding var videoURL: URL?
func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
let playerController = AVPlayerViewController()
playerController.modalPresentationStyle = .fullScreen
playerController.player = playerState.player(for: videoURL!)
playerController.player?.play()
return playerController
}
}
Something to be aware of (a bug?): whenever a modal sheet is shown using .sheet() the environment objects are not automatically passed to the subviews. They have to be added using environmentObject().
Here is a link to read more about this problem: https://oleb.net/2020/sheet-environment/
Xcode 12 · iOS 14
→ Use .fullScreenCover instead of .sheet and you’re good to go.
See also: How to present a full screen modal view using fullScreenCover

Change to #Published var in #EnvironmentObject not reflected immediately

In this specific case, when I try to change an #EnvironmentObject's #Published var, I find that the view is not invalidated and updated immediately. Instead, the change to the variable is only reflected after navigating away from the modal and coming back.
import SwiftUI
final class UserData: NSObject, ObservableObject {
#Published var changeView: Bool = false
}
struct MasterView: View {
#EnvironmentObject var userData: UserData
#State var showModal: Bool = false
var body: some View {
Button(action: { self.showModal.toggle() }) {
Text("Open Modal")
}.sheet(isPresented: $showModal, content: {
Modal(showModal: self.$showModal)
.environmentObject(self.userData)
} )
}
}
struct Modal: View {
#EnvironmentObject var userData: UserData
#Binding var showModal: Bool
var body: some View {
VStack {
if userData.changeView {
Text("The view has changed")
} else {
Button(action: { self.userData.changeView.toggle() }) {
Text("Change View")
}
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MasterView().environmentObject(UserData())
}
}
#endif
Is this a bug or am I doing something wrong?
This works if changeView is a #State var inside Modal. It also works if it's a #State var inside MasterView with a #Binding var inside Modal. It just doesn't work with this setup.
A couple of things.
Your setup doesn't work if you move the Button into MasterView either.
You don't have a import Combine in your code (don't worry, that alone doesn't help).
Here's the fix. I don't know if this is a bug, or just poor documentation - IIRC it states that objectWillChange is implicit.
Along with adding import Combine to your code, change your UserData to this:
final class UserData: NSObject, ObservableObject {
var objectWillChange = PassthroughSubject<Void, Never>()
#Published var changeView: Bool = false {
willSet {
objectWillChange.send()
}
}
}
I tested things and it works.
Changing
final class UserData: NSObject, ObservableObject {
to
final class UserData: ObservableObject {
does fix the issue in Xcode11 Beta6. SwiftUI does seem to not handle NSObject subclasses implementing ObservableObject correctly (at least it doesn't not call it's internal willSet blocks it seems).
In Xcode 11 GM2, If you have overridden objectWillChange, then it needs to call send() on setter of a published variable.
If you don't overridden objectWillChange, once the published variables in #EnvironmentObject or #ObservedObject change, the view should be refreshed. Since in Xcode 11 GM2 objectWillChange already has a default instance, it is no longer necessary to provide it in the ObservableObject.