ForEach in SwiftUI Warning Issue - swiftui

I have been using SwiftUI for a few months now an I am having difficulty with using ForEach.
I am aware that the ForEach protocol demands a unique identifier, but I have used /.self to overcome that aspect of the protocol.
Now unit testing a ForEach statement but I am getting a warning which is preventing build.
Warning is Result of 'ForEach' initializer is
unused
import SwiftUI
struct GetdOrderView: View {
#State private var myFamily = ["Ufuoma","Efe","David","Vicky","Beth"]
//The use of ForEach
func myForachOne() {
ForEach((0 ... myFamily.count), id: \.self) {member in
VStack {
Text("\(member)")
}
}
}
var body: some View {
Text("Hello world")
}
}

Instead of
func myForachOne() {
Use
func myForachOne() -> some View {

//Use This
import SwiftUI
struct GetdOrderView: View {
#State private var myFamily = ["Ufuoma","Efe","David","Vicky","Beth"]
//The use of ForEach
func myForachOne() -> some View {
ForEach((0 ... myFamily.count), id: \.self) {member in
VStack {
Text("\(member)")
}
}
}
var body: some View {
Text("Hello world")
}
}

Related

SwiftUI: Cannot find type in scope

I'm having trouble coming up a good way to ask this question, so I'll instead show a simple example. I have a model, an #ObservableObject, that contains a struct:
class MyModel: ObservableObject {
#Published var allData: [TheData] = []
struct TheData: Hashable {
let thePosition: Int
let theChar: Character
}
func initState() {
let allChars = Array("abd")
for (index, element) in allChars.enumerated() {
allData.append(TheData(thePosition: index, theChar: element))
}
}
}
In my view, I'm attempting to reach the model from two different structs (as a result of an annoying The compiler is unable to type-check this expression in reasonable time..), but I get an error:
struct ContentView: View {
#StateObject var theModel = MyModel()
var body: some View {
VStack {
Image(systemName: "globe")
Text("Hello, world!")
HStack {
ForEach(theModel.allData, id: \.self) { theElement in
letterAcross(myLetter: theElement)
}
}
}
.onAppear {
theModel.initState()
}
.environmentObject(theModel)
}
}
struct letterAcross: View {
#EnvironmentObject var theModel: MyModel
var myLetter: TheData // <----- the ERROR is here
var body: some View {
HStack {
Text(String(myLetter.theChar))
}
}
}
The error is Cannot find type TheData in scope. It appears I am somehow messing up the #StateObject and #EnvironmentObject. What is the correct way to do this?
It's a nested type, so it's MyModel.TheData:
var myLetter: MyModel.TheData

SwiftUI: How to open a new window with URL Schemes and pass an FetchedResult?

following this really good tutorial (https://crystalminds.medium.com/programmatically-open-a-new-window-in-swiftui-on-macos-ventura-13-804ab6de20a9) I'd like to pass an FetchedResult to the new window.
What I did so far:
Created a NavigationSplitView that displays all my FetchedResults from my Table "Klienten" following the Apple Documentation for NavigationSplitView.
NavigationSplitView {
List(klienten, selection: $selectedClient) { klient in
Text("\(klient.nachname ?? "kein Name"), \(klient.vorname ?? "kein Name")").tag(klient)
}
.navigationTitle("Clients")
.toolbar {
ToolbarItem {
Button {
if let url = URL(string: "justcare://klient=\(selectedClient)") {
NSWorkspace.shared.open(url)
}
} label: {
Label("Edit Item", systemImage: "pencil")
}
.disabled(selectedClient != nil ? false : true)
}
}
.searchable(text: $filter)
} detail: {
if selectedClient != nil {
ItemDetailView(klient: selectedClient!)
}
}
.navigationSplitViewStyle(.prominentDetail)
Created a new URL Type named "justcare"
Created a new View for editing
struct EditClientView: View {
#ObservedObject var klient: Klienten
var body: some View {
Text(klient!.nachname ?? "Kein Nachname")
}
}
Created a new Scene
WindowGroup {
EditClientView(klient: nil)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
.handlesExternalEvents(matching: ["justcare"])
And here's my problem: At line 2 I can't pass nil to parameter klient. In my EditClientView I can't make the ObservedObject optional.
Additionally the line URL(string: "justcare://klient=\(selectedClient)") causes problems, because selectedClient is, of course, not a String. :-)
How can I pass the selected Klienten Object in order to edit it?
Thanks for advice.
Edit:
In German, there's a idiom like "From behind through the chest into the eye". Don't know, if this is the proper way, but I set the variable in my App-Struct and binded it through to my View.
struct justCare_2App: App {
let persistenceController = PersistenceController.shared
#State private var selectedKlient: Klienten?
var body: some Scene {
WindowGroup {
ContentView(selectedClient: $selectedKlient)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
WindowGroup {
EditClientView(klient: $selectedKlient)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
.handlesExternalEvents(matching: ["justcare"])
}
}
And in my ContentView:
struct ContentView: View {
#Environment(\.managedObjectContext) private var moc
#Binding var selectedClient: Klienten?
#State var filter: String = ""
init(selectedClient: Binding<Klienten?>) {
_selectedClient = selectedClient
}
[...]
}
Is there a more comfortable or accurate way? Or can it be done this way?

SwiftUI publishing an environment change from within view update

The app has a model that stores the user's current preference for light/dark mode, which the user can change by clicking on a button:
class DataModel: ObservableObject {
#Published var mode: ColorScheme = .light
The ContentView's body tracks the model, and adjusts the colorScheme when the model changes:
struct ContentView: View {
#StateObject private var dataModel = DataModel()
var body: some View {
NavigationStack(path: $path) { ...
}
.environmentObject(dataModel)
.environment(\.colorScheme, dataModel.mode)
As of Xcode Version 14.0 beta 5, this is producing a purple warning: Publishing changes from within view updates is not allowed, this will cause undefined behavior. Is there another way to do this? Or is it a hiccup in the beta release? Thanks!
Update: 2022-09-28
Xcode 14.1 Beta 3 (finally) fixed the "Publishing changes from within view updates is not allowed, this will cause undefined behavior"
See: https://www.donnywals.com/xcode-14-publishing-changes-from-within-view-updates-is-not-allowed-this-will-cause-undefined-behavior/
Full disclosure - I'm not entirely sure why this is happening but these have been the two solutions I have found that seem to work.
Example Code
// -- main view
#main
struct MyApp: App {
#StateObject private var vm = ViewModel()
var body: some Scene {
WindowGroup {
ViewOne()
.environmentObject(vm)
}
}
}
// -- initial view
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
// -- sheet view
struct SheetView: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Close sheet")
}
}
}
// -- view model
class ViewModel: ObservableObject {
#Published var isPresented: Bool = false
}
Solution 1
Note: from my testing and the example below I still get the error to appear. But if I have a more complex/nested app then the error disappears..
Adding a .buttonStyle() to the button that does the initial toggling.
So within the ContentView on the Button() {} add in a .buttonStyle(.plain) and it will remove the purple error:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.buttonStyle(.plain) // <-- here
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
^ This is probably more of a hack than solution since it'll output a new view from the modifier and that is probably what is causing it to not output the error on larger views.
Solution 2
This one is credit to Alex Nagy (aka. Rebeloper)
As Alex explains:
.. with SwiftUI 3 and SwiftUI 4 the data handling kind of changed. How SwiftUI handles, more specifically the #Published variable ..
So the solution is to have the boolean trigger to be a #State variable within the view and not as a #Published one inside the ViewModel. But as Alex points out it can make your views messy and if you have a lot of states in it, or not be able to deep link, etc.
However, since this is the way that SwiftUI 4 wants these to operate, we run the code as such:
// -- main view
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ViewOne()
}
}
}
// -- initial view
struct ViewOne: View {
#State private var isPresented = false
var body: some View {
Button {
isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
// SheetView() <-- if using dismiss() in >= iOS 15
}
}
}
// -- sheet view
struct SheetView: View {
// I'm showing a #Binding here for < iOS 15
// but you can use the dismiss() option if you
// target higher
// #Environment(\.dismiss) private var dismiss
#Binding var isPresented: Bool
var body: some View {
Button {
isPresented.toggle()
// dismiss()
} label: {
Text("Close sheet")
}
}
}
Using the #Published and the #State
Continuing from the video, if you need to still use the #Published variable as it might tie into other areas of your app you can do so with a .onChange and a .onReceive to link the two variables:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.onReceive(vm.$isPresented) { newValue in
isPresented = newValue
}
.onChange(of: isPresented) { newValue in
vm.isPresented = newValue
}
}
}
However, this can become really messy in your code if you have to trigger it for every sheet or fullScreenCover.
Creating a ViewModifier
So to make it easier for you to implement it you can create a ViewModifier which Alex has shown works too:
extension View {
func sync(_ published: Binding<Bool>, with binding: Binding<Bool>) -> some View {
self
.onChange(of: published.wrappedValue) { newValue in
binding.wrappedValue = newValue
}
.onChange(of: binding.wrappedValue) { newValue in
published.wrappedValue = newValue
}
}
}
And in use on the View:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.sync($vm.isPresented, with: $isPresented)
// .onReceive(vm.$isPresented) { newValue in
// isPresented = newValue
// }
// .onChange(of: isPresented) { newValue in
// vm.isPresented = newValue
// }
}
}
^ Anything denoted with this is my assumptions and not real technical understanding - I am not a technical knowledgeable :/
Try running the code that's throwing the purple error asynchronously, for example, by using DispatchQueue.main.async or Task.
DispatchQueue.main.async {
// environment changing code comes here
}
Task {
// environment changing code comes here
}
Improved Solution of Rebel Developer
as a generic function.
Rebeloper solution
It helped me a lot.
1- Create extension for it:
extension View{
func sync<T:Equatable>(_ published:Binding<T>, with binding:Binding<T>)-> some View{
self
.onChange(of: published.wrappedValue) { published in
binding.wrappedValue = published
}
.onChange(of: binding.wrappedValue) { binding in
published.wrappedValue = binding
}
}
}
2- sync() ViewModel #Published var to local #State var
struct ContentView: View {
#EnvironmentObject var viewModel:ViewModel
#State var fullScreenType:FullScreenType?
var body: some View {
//..
}
.sync($viewModel.fullScreenType, with: $fullScreenType)

SwiftUI - Subclassed viewModel doesn't trigger view refresh

I have this situation where I have a a BaseView containing some common elements and a BaseViewModel containing some common functions, but when its #Published var get updated no BaseView refresh occurs.
The setup is this:
class BaseViewModel: ObservableObject {
#Published var overlayView: AnyView = EmptyView().convertToAnyView()
func forceViewRefresh() {
self.objectWillChange.send()
}
func setOverlayView(overlayView: AnyView) {
self.overlayView = overlayView
}
}
This view model subclasses BaseViewModel:
class FirstViewModel: BaseViewModel {
func showOverlayView() {
self.setOverlayView(overlayView: OverlayView().convertToAnyView())
}
}
also I have a BaseView where I use the overlayView
struct BaseView<Content: View>: View {
let content: Content
#ObservedObject var viewModel = BaseViewModel()
init(content: () -> Content) {
self.content = content()
}
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.vertical)
content
viewModel.overlayView
}
}
}
The first view that gets displayed is FirstView, which conforms to a BaseViewProtocol and has a FirstViewModel that extends BaseViewModel.
struct FirstView: BaseViewProtocol {
#ObservedObject var viewModel = FirstViewModel()
var body: some View {
BaseView() {
Button("Show overlay") {
viewModel.showOverlayView()
}
}
}
}
Clicking the Show overlay button in First View calls the showOverlayView() func on FirstViewModel which in turn calls setOverlayView on the BaseViewModel. The value of overlayView in BaseViewModel changes as expected, but no view refresh on FirstView is called.
What am I doing wrong?
Thanks a lot.
I have just tested this code sample and works fine on Xcode 12 beta 6 & iOS 14 beta 8
struct ContentView: View {
#StateObject private var viewModel = FirstViewModel()
var body: some View {
ZStack {
Button(action: { viewModel.showOverlayView() }) {
Text("Press")
}
viewModel.overlayView
}
}
}
class BaseViewModel: ObservableObject {
#Published var overlayView: AnyView = AnyView(EmptyView())
func forceViewRefresh() {
self.objectWillChange.send()
}
func setOverlayView(overlayView: AnyView) {
self.overlayView = overlayView
}
}
class FirstViewModel: BaseViewModel {
func showOverlayView() {
self.setOverlayView(
overlayView: AnyView(
Color.blue
.opacity(0.2)
.allowsHitTesting(false)
)
)
}
}
Generally in SwiftUI you don't create views in outside the body. The view creation should be left to SwiftUI - instead you can define some other controls telling SwiftUI how and when to create a view.
Here is a simplified demo how to present different overlays for different views.
You can create a basic OverlayView:
enum OverlayType {
case overlay1, overlay2
}
struct OverlayView: View {
let overlayType: OverlayType
#ViewBuilder
var body: some View {
if overlayType == .overlay1 {
Text("Overlay1") // can be replaced with any view you want
}
if overlayType == .overlay2 {
Text("Overlay1")
}
}
}
and use it in your BaseView (if overlayType is nil the overlay will not be shown):
struct BaseView<Content>: View where Content: View {
let overlayType: OverlayType?
let content: () -> Content
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.vertical)
content()
if overlayType != nil {
OverlayView(overlayType: overlayType!)
}
}
}
}
Now in the ContentView you can use the BaseView and specify its OverlayType.
struct ContentView: View {
#State var overlayType: OverlayType?
var body: some View {
BaseView(overlayType: overlayType) {
Button("Show overlay") {
overlayType = .overlay1
}
}
}
}
Some considerations:
For simplicity I used #State variables to control overlays. If there are other use cases for your ViewModels you may want to move the logic there.
Note that instead of AnyView it's preferred to use #ViewBuilder.
Also if you want to observe an ObservableObject inside a view, you need to use #ObservedObject, not #ObservableObject.

The correct way to list CoreData object in nest NavigationView in NavigationLink

In xcode 12 (swift 5.3), I using the conditional navigationLink navigate to another navigationView to list coreData object with NavigationLink. But it seems the AnotherView's NavigationTitle can not be correctly show at the top of screen, instead it padding to the top. The list in another navigationView have a external white background color. The something.id which I want to pass to SomethingView report Argument passed to call that takes no arguments error, but I can get something.name in Text.
struct StartView: View {
#State var changeToAnotherView: String? = nil
var body: some View {
NavigationView {
VStack(spacing: 20) {
...
NavigationLink(destination: AnotherView(), tag: "AnotherView",
selection: $changeToAnotherView) { EmptyView() }
}
}
}
}
struct AnotherView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Something.entity(), sortDescriptors: []) var somethings: FetchedResults<Something>
...
var body: some View {
NavigationView {
List {
ForEach(self.somethings, id: \.id) { something in
NavigationLink(destination: SomethingView(somethingID: something.id)) {
Text(something.name ?? "unknown name")
}
}
}
.navigationBarTitle("SomethingList")
}
}
}
You don't need second NavigationView - it must be only one in view hierarchy, as well it is better to pass CoreData object by reference (view will be able to observe it), so
struct AnotherView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Something.entity(), sortDescriptors: []) var somethings: FetchedResults<Something>
...
var body: some View {
List {
ForEach(self.somethings, id: \.id) { something in
NavigationLink(destination: SomethingView(something: something)) {
Text(something.name ?? "unknown name")
}
}
}
.navigationBarTitle("SomethingList")
}
}
struct SomethingView: View {
#ObservedObject var something: Something
var body: some View {
// .. your next code