I'm in the midst of moving my app from iOS 15 to iOS 16.
In my app ( iOS 15 ) I have a LogOn View (sysPassView) that automatically pops on first entry into my Navigation View using the following code:
NavigationView {
#State private var showLogin: Bool = true
NavigationLink("", destination: sysPassView( showLogin: $showLogin).environmentObject(defaults), isActive: $showLogin)
EmptyView()
}
In moving my app to iOS 16, I am getting this message related to the NavigationLink:
'init(:destination:isActive:)' was deprecated in iOS 16.0: use NavigationLink(:value:) inside a NavigationStack or NavigationSplitView
I made the following changes to convert my NavigationView to a NavigationStack:
#State private var showLogin: Bool = true
NavigationStack {
.navigationDestination(isPresented: $showLogin) {
sysPassView(showLogin: $showLogin).environmentObject(defaults)
}
This is not automatically popping the Logon View as it did with the NavigationLink.
Appreciate any suggestions on how to address this.
#State private var showLogin: Bool = false
NavigationStack {
VStack {
Button("Show Login") {
showLogin = true
}
}
.navigationDestination(isPresented: $showLogin) {
sysPassView(showLogin: $showLogin).environmentObject(defaults)
}
}
Related
I am using the new concept AppShell, and I am doing the following flow
App.MainPage = new AppShell();
then I do
protected async override void OnStart()
{
await Shell.Current.GoToAsync($"//{nameof(LoginPage)}");
}
With this I have a ForgetPasswordPage, from LoginViewModel I do
await Shell.Current.GoToAsync($"//{nameof(ForgetPasswordPage)}");
The point is if I do this
var color = new Color(33, 150, 243);
Xamarin.Essentials.Platform.CurrentActivity.Window.SetStatusBarColor(color);
Inside the Create method from Android the StatusBar has the color I defined.
Now where is the method for iOS, like this?
I tried theses solutions
How to change the status bar color without a navigation page
Xamarin.Forms.Shell: how to manage StatusBar color
https://github.com/georgemichailou/ShaXam/
Without succeed...
Someone can explain, how to change the status code but I would like to set it inside my ContentPage, I could use a style, or a simple line of code like this
Xamarin.Essentials.Platform.CurrentActivity.Window.SetStatusBarColor(color);
Which is used in Android project.
I did the steps from the other posts, and I got:
Foundation.MonoTouchException: 'Objective-C exception thrown. Name: NSInternalInconsistencyException Reason: App called -statusBar or -statusBarWindow on UIApplication: this code must be changed as there's no longer a status bar or status bar window. Use the statusBarManager object on the window scene instead.
Native stack trace:
0 CoreFoundation 0x00007fff20422fba __exceptionPreprocess + 242
System.Reflection.TargetInvocationException Message=Exception has been thrown by the target of an invocation
You can take a look at my video here that walks through in details: https://www.youtube.com/watch?v=GKJRR8_DSSs
In general:
Android -
public void SetStatusBarColor(System.Drawing.Color color, bool darkStatusBarTint)
{
if (Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.Lollipop)
return;
var activity = Platform.CurrentActivity;
var window = activity.Window;
window.AddFlags(Android.Views.WindowManagerFlags.DrawsSystemBarBackgrounds);
window.ClearFlags(Android.Views.WindowManagerFlags.TranslucentStatus);
window.SetStatusBarColor(color.ToPlatformColor());
if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.M)
{
var flag = (Android.Views.StatusBarVisibility)Android.Views.SystemUiFlags.LightStatusBar;
window.DecorView.SystemUiVisibility = darkStatusBarTint ? flag : 0;
}
}
iOS:
public void SetStatusBarColor(System.Drawing.Color color, bool darkStatusBarTint)
{
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
{
var statusBar = new UIView(UIApplication.SharedApplication.KeyWindow.WindowScene.StatusBarManager.StatusBarFrame);
statusBar.BackgroundColor = color.ToPlatformColor();
UIApplication.SharedApplication.KeyWindow.AddSubview(statusBar);
}
else
{
var statusBar = UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) as UIView;
if (statusBar.RespondsToSelector(new ObjCRuntime.Selector("setBackgroundColor:")))
{
statusBar.BackgroundColor = color.ToPlatformColor();
}
}
var style = darkStatusBarTint ? UIStatusBarStyle.DarkContent : UIStatusBarStyle.LightContent;
UIApplication.SharedApplication.SetStatusBarStyle(style, false);
Xamarin.Essentials.Platform.GetCurrentUIViewController()?.SetNeedsStatusBarAppearanceUpdate();
}
I'm using Google Firebase's Cloud Firestore service with a SwiftUI project that details strategies in a video game on a per-map basis. I'm using a View, ViewModel, Model architecture. It might also be worth noting that I'm using the Swift Package Manager to import the APIs rather than CocoaPods.
I have a ScrollView with a few maps fetched from a Firestore Collection. Tapping on the map will present the user with another ScrollView listing the strategies fetched from a separate collection in Firestore. The issue I'm experiencing is that, whenever a UserDefault is changed (either using UserDefaults.standard.setValue(...) or #AppStorage("settingName") var...) the contents of the ScrollView disappear. There's no relationship between the Firestore data and UserDefaults (nothing being saved from one to the other).
This only happens to the detail view, not the main view, despite the code being copied and pasted from one ViewModel swift file to the other.
Here's the StratsViewModel.swift code:
class StratsViewModel: ObservableObject {
#Published var strats = [Strat]()
private var db = Firestore.firestore()
func fetchData(forMap: String) {
if strats.isEmpty {
db.collection("maps/\(forMap)/strats").getDocuments { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
return
}
self.strats = documents.map { (queryDocumentSnapshot) -> Strat in
let data = queryDocumentSnapshot.data()
let id = queryDocumentSnapshot.documentID
let name = data["name"] as? String ?? ""
let map = data["map"] as? String ?? ""
let type = data["type"] as? String ?? ""
let side = data["side"] as? String ?? ""
let strat = Strat(id: id, name: name, map: map, type: type, side: side)
return strat
}
}
}
}
}
In the MapsDetailView.swift (presented by MapsView.swift) ScrollView, the strategies are displayed like so:
struct MapsDetailView: View {
var map: Map
#ObservedObject private var viewModel = StratsViewModel()
var body: some View {
ScrollView {
ForEach(viewModel.strats, id: \.self) { strat in
NavigationLink(destination: StratView()) {
StratCell(strat: strat)
}
}
}
.onAppear() {
self.viewModel.fetchData(forMap: map.id)
}
}
}
If I were to add a button to the view, for example, that set any value to any key in UserDefaults, the dozens of items in the ScrollView disappear.
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
UserDefaults.standard.setValue("test", forKey: "testKey")
} label: {
Text("Don't press me!")
}
}
}
Using the TabBar to navigate to another view, and then back to the MapDetailView will trigger the .onAppear method however, adding a breakpoint to the ForEach line shown above, reveals that this loop doesn't run again until the view is entirely dismissed and reopened.
The issue also crops up when switching tabs in the TabBar as, onAppear, each view sets a tabSelection key to remember which tab the user last selected when the app is killed. Switching from one tab, back to the MapsDetailView tab will remove all cells in the ScrollView.
Any thoughts? Happy to share more source code if necessary.
When you initialize the viewModel, you should use #StateObject instead of #ObservedObject. The two property wrappers are almost the same, except the #StateObject will make sure that the object stays "alive" when the view is updated / re-rendered, rather than re-initializing. Here are some great resources on this topic:
What is #StateObject?
#ObservedObject vs #StateObject vs #EnvironmentObject
TL;DR
Can't seem to use binding to tell wrapped AVPlayer to stop — why not? The "one weird trick" from Vlad works for me, without state & binding, but why?
See Also
My question is something like this one but that poster wanted to wrap an AVPlayerViewController and I want to control playback programmatically.
This guy also wondered when updateUIView() was called.
What happens (Console logs shown below.)
With code as shown here,
The user taps "Go to Movie"
MovieView appears and the vid plays
This is because updateUIView(_:context:) is being called
The user taps "Go back Home"
HomeView reappears
Playback halts
Again updateUIView is being called.
See Console Log 1
But... remove the ### line, and
Playback continues even when the home view returns
updateUIView is called on arrival but not departure
See Console log 2
If you uncomment the %%% code (and comment out what precedes it)
You get code I thought was logically and idiomatically correct SwiftUI...
...but "it doesn't work". I.e. the vid plays on arrival but continues on departure.
See Console log 3
The code
I do use an #EnvironmentObject so there is some sharing of state going on.
Main content view (nothing controversial here):
struct HomeView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
ZStack() { // +++ Weird trick ### fails if this is Group(). Wtf?
if router.page == .home {
Button(action: { self.router.page = .movie }) {
Text("Go to Movie")
}
} else if router.page == .movie {
MovieView()
}
}
}
}
which uses one of these (still routine declarative SwiftUI):
struct MovieView: View {
#EnvironmentObject var router: ViewRouter
// #State private var isPlaying: Bool = false // %%%
var body: some View {
VStack() {
PlayerView()
// PlayerView(isPlaying: $isPlaying) // %%%
Button(action: { self.router.page = .home }) {
Text("Go back Home")
}
}.onAppear {
print("> onAppear()")
self.router.isPlayingAV = true
// self.isPlaying = true // %%%
print("< onAppear()")
}.onDisappear {
print("> onDisappear()")
self.router.isPlayingAV = false
// self.isPlaying = false // %%%
print("< onDisappear()")
}
}
}
Now we get into the AVKit-specific stuff. I use the approach described by Chris Mash.
The aforementioned PlayerView, the wrappER:
struct PlayerView: UIViewRepresentable {
#EnvironmentObject var router: ViewRouter
// #Binding var isPlaying: Bool // %%%
private var myUrl : URL? { Bundle.main.url(forResource: "myVid", withExtension: "mp4") }
func makeUIView(context: Context) -> PlayerView {
PlayerUIView(frame: .zero , url : myUrl)
}
// ### This one weird trick makes OS call updateUIView when view is disappearing.
class DummyClass { } ; let x = DummyClass()
func updateUIView(_ v: PlayerView, context: UIViewRepresentableContext<PlayerView>) {
print("> updateUIView()")
print(" router.isPlayingAV = \(router.isPlayingAV)")
// print(" isPlaying = \(isPlaying)") // %%%
// This does work. But *only* with the Dummy code ### included.
// See also +++ comment in HomeView
if router.isPlayingAV { v.player?.pause() }
else { v.player?.play() }
// This logic looks reversed, but is correct.
// If it's the other way around, vid never plays. Try it!
// if isPlaying { v?.player?.play() } // %%%
// else { v?.player?.pause() } // %%%
print("< updateUIView()")
}
}
And the wrappED UIView:
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
var player: AVPlayer?
init(frame: CGRect, url: URL?) {
super.init(frame: frame)
guard let u = url else { return }
self.player = AVPlayer(url: u)
self.playerLayer.player = player
self.layer.addSublayer(playerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
required init?(coder: NSCoder) { fatalError("not implemented") }
}
And of course the view router, based on the Blckbirds example
class ViewRouter : ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter, Never>()
enum Page { case home, movie }
var page = Page.home { didSet { objectWillChange.send(self) } }
// Claim: App will never play more than one vid at a time.
var isPlayingAV = false // No didSet necessary.
}
Console Logs
Console log 1 (playing stops as desired)
> updateUIView() // First call
router.isPlayingAV = false // Vid is not playing => play it.
< updateUIView()
> onAppear()
< onAppear()
> updateUIView() // Second call
router.isPlayingAV = true // Vid is playing => pause it.
< updateUIView()
> onDisappear() // After the fact, we clear
< onDisappear() // the isPlayingAV flag.
Console log 2 (weird trick disabled; playing continues)
> updateUIView() // First call
router.isPlayingAV = false
< updateUIView()
> onAppear()
< onAppear()
// No second call.
> onDisappear()
< onDisappear()
Console log 3 (attempt to use state & binding; playing continues)
> updateUIView()
isPlaying = false
< updateUIView()
> onAppear()
< onAppear()
> updateUIView()
isPlaying = true
< updateUIView()
> updateUIView()
isPlaying = true
< updateUIView()
> onDisappear()
< onDisappear()
Well... on
}.onDisappear {
print("> onDisappear()")
self.router.isPlayingAV = false
print("< onDisappear()")
}
this is called after view is removed (it is like didRemoveFromSuperview, not will...), so I don't see anything bad/wrong/unexpected in that subviews (or even it itself) is not updated (in this case updateUIView)... I would rather surprise if it would be so (why update view, which is not in view hierarchy?!).
So this
class DummyClass { } ; let x = DummyClass()
is rather some wild bug, or ... bug. Forget about it and never use such stuff in releasing products.
OK, one would now ask, how to do with this? The main issue I see here is design-originated, specifically tight-coupling of model and view in PlayerUIView and, as a result, impossibility to manage workflow. AVPlayer here is not part of view - it is model and depending on its states AVPlayerLayer draws content. Thus the solution is to tear apart those entities and manage separately: views by views, models by models.
Here is a demo of modified & simplified approach, which behaves as expected (w/o weird stuff and w/o Group/ZStack limitations), and it can be easily extended or improved (in model/viewmodel layer)
Tested with Xcode 11.2 / iOS 13.2
Complete module code (can be copy-pasted in ContentView.swift in project from template)
import SwiftUI
import Combine
import AVKit
struct MovieView: View {
#EnvironmentObject var router: ViewRouter
// just for demo, but can be interchangable/modifiable
let playerModel = PlayerViewModel(url: Bundle.main.url(forResource: "myVid", withExtension: "mp4")!)
var body: some View {
VStack() {
PlayerView(viewModel: playerModel)
Button(action: { self.router.page = .home }) {
Text("Go back Home")
}
}.onAppear {
self.playerModel.player?.play() // << changes state of player, ie model
}.onDisappear {
self.playerModel.player?.pause() // << changes state of player, ie model
}
}
}
class PlayerViewModel: ObservableObject {
#Published var player: AVPlayer? // can be changable depending on modified URL, etc.
init(url: URL) {
self.player = AVPlayer(url: url)
}
}
struct PlayerView: UIViewRepresentable { // just thing wrapper, as intended
var viewModel: PlayerViewModel
func makeUIView(context: Context) -> PlayerUIView {
PlayerUIView(frame: .zero , player: viewModel.player) // if needed viewModel can be passed completely
}
func updateUIView(_ v: PlayerUIView, context: UIViewRepresentableContext<PlayerView>) {
}
}
class ViewRouter : ObservableObject {
enum Page { case home, movie }
#Published var page = Page.home // used native publisher
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
var player: AVPlayer?
init(frame: CGRect, player: AVPlayer?) { // player is a model so inject it here
super.init(frame: frame)
self.player = player
self.playerLayer.player = player
self.layer.addSublayer(playerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
required init?(coder: NSCoder) { fatalError("not implemented") }
}
struct ContentView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
Group {
if router.page == .home {
Button(action: { self.router.page = .movie }) {
Text("Go to Movie")
}
} else if router.page == .movie {
MovieView()
}
}
}
}
I'm populating a List with a Realm Result set.
When navigating from this list it opens a new view then automatically closes that view.
Using a struct presents no issue.
Why would the second view automatically close?
I have a screen recording but cant post here.
import SwiftUI
import Combine
struct TestStruct:Identifiable{
let id = UUID()
let firstname: String
}
extension TestStruct {
static func all() -> [TestStruct]{
return[
TestStruct(firstname: "Joe"),
TestStruct(firstname: "Jane"),
TestStruct(firstname: "Johns")
]
}
}
struct TestListView: View {
let realmList = Horoscope.getHoroscopes() //Fetches from Realm
let structList = TestStruct.all()
var body: some View {
NavigationView{
// This owrks
// List(structList) { item in
// MyItemRow(itemTxt: item.firstname)
// }
//This automatically closes the view
List(realmList) { item in
MyItemRow(itemTxt: item.firstname)
}
.navigationBarTitle("Charts", displayMode: .automatic)
.navigationBarItems(trailing: EditButton())
}
}
}
struct MyItemRow: View {
var itemTxt:String
var body: some View {
NavigationLink(destination: Text("Test")) {
Text(itemTxt)
}
}
}
struct TestListView_Previews: PreviewProvider {
static var previews: some View {
TestListView()
}
}
I think the answer can be found here
In short, do not generate the id of the collection on which the ForEach iterates. It would detect a change and navigate back.
Realm object has an auto generated id property with each reference, try replacing it with a consistent id
The following solution worked for me.
The code with an issue (specifying id: \.self is the root cause since it uses the hash calculated from all objects the Stream object consists of, including the data that lies in a subarray).
...
List(streams, id: \.self) { stream in
...
The code with no issues:
...
List(streams, id: \._id) { stream in
// or even List(streams) { stream in
...
The streams is a #ObservedResults(Stream.self) var streams and the object scheme is:
final class Stream: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var title: String
#Persisted var subtitle: String?
#Persisted var topics = RealmSwift.List<Topic>()
// tags, etc.
}
The issue happened when I added new topic at the topics list in the first stack of the navigationView.
I'm creating a new watchOS app using SwiftUI and Combine trying to use a MVVM architecture, but when my viewModel changes, I can't seem to get a Text view to update in my View.
I'm using watchOS 6, SwiftUI and Combine. I am using #ObservedObject and #Published when I believe they should be used, but changes aren't reflected like I would expect.
// Simple ContentView that will push the next view on the navigation stack
struct ContentView: View {
var body: some View {
NavigationLink(destination: NewView()) {
Text("Click Here")
}
}
}
struct NewView: View {
#ObservedObject var viewModel: ViewModel
init() {
viewModel = ViewModel()
}
var body: some View {
// This value never updates
Text(viewModel.str)
}
}
class ViewModel: NSObject, ObservableObject {
#Published var str = ""
var count = 0
override init() {
super.init()
// Just something that will cause a property to update in the viewModel
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.count += 1
self?.str = "\(String(describing: self?.count))"
print("Updated count: \(String(describing: self?.count))")
}
}
}
Text(viewModel.str) never updates, even though the viewModel is incrementing a new value ever 1.0s. I have tried objectWillChange.send() when the property updates, but nothing works.
Am I doing something completely wrong?
For the time being, there is a solution that I luckily found out just by experimenting. I'm yet to find out what the actual reason behind this. Until then, you just don't inherit from NSObject and everything should work fine.
class ViewModel: ObservableObject {
#Published var str = ""
var count = 0
init() {
// Just something that will cause a property to update in the viewModel
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.count += 1
self?.str = "\(String(describing: self?.count))"
print("Updated count: \(String(describing: self?.count))")
}
}
}
I've tested this and it works.
A similar question also addresses this issue of the publisher object being a subclass of NSObject. So you may need to rethink if you really need an NSObject subclass or not. If you can't get away from NSObject, I recommend you try one of the solutions from the linked question.