SwiftUI - Trigger a Modal Sheet from an ObservableObject? - view

I've created a random number generator that should present a new sheet if the number 3 appears.
The logic is in a separate class but when I use .sheet or .fullScreenCover on the ContentView it doesn't work.
Is it possible to trigger a modal sheet from an ObservableObject in Xcode12 / iOS 14 SwiftUI?
Minimal reproducible example below:
import SwiftUI
struct ContentView: View {
#StateObject var mathLogic = MathLogic()
var body: some View {
VStack{
Text(String(mathLogic.newNumber))
.padding(.bottom, 40)
Text("Tap for a number")
.onTapGesture{
mathLogic.generateRandomNumber()
}
}
.fullScreenCover(isPresented: mathLogic.$isLucky3, content: NewModalView.init)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct NewModalView: View {
var body: some View {
Text("You hit lucky number 3!")
}
}
class MathLogic: ObservableObject {
#Published var newNumber = 0
#State var isLucky3 = false
func generateRandomNumber() {
newNumber = Int.random(in: 1..<5)
guard self.newNumber != 3 else {
// trigger modal
self.isLucky3.toggle()
return
}
}
}

The #State is intended to be in View, in ObservableObject we use #Published, so it should be
class MathLogic: ObservableObject {
#Published var newNumber = 0
#Published var isLucky3 = false // << here !!
// .. other code
and to bind it via ObservedObject, because .$isLucky3 gives publisher
// ... other code
}
.fullScreenCover(isPresented: $mathLogic.isLucky3, content: NewModalView.init)
Tested with Xcode 12 / iOS 14

Related

How to fix Type 'ContentView' does not conform to protocol 'View' etc. XCODE - SwiftUI

import SwiftUI
import CodeScanner
struct ContentView: View {
#State var isPresentingScanner = false
#State var scannedCode: String = "Scan a QR code to get started."
var scannerSheet : some View {
CodeScannerView(
codeTypes: [.qr],
completion:{ result in
if case let.success(code) = result {
self.scannedCode = code.string
self.isPresentingScanner = false
}
}
)
var body: some View {
VStack(spacing: 10) {
Text(scannedCode)
Button("Scan QR code") {
self.isPresentingScanner = true
}
.sheet(isPresented: $isPresentingScanner) {
self.scannerSheet
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
I'm a very new beginner in XCODE and am a bit stuck with these errors shown on my code and I've tried looking up solutions but I cannot get anywhere, unfortunately. I would appreciate any help or feedback.
The errors that are occuring are "Type 'ContentView' does not conform to protocol 'View'", "Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type", "Result of 'CodeScannerView' initializer is unused"
P.S I am attempting to create a code scanner and I already have a packaged added.
enter image description here
It's because you didn't put closing brackets for scannerSheet, and had extra closing brackets after the body variable.
This should work.
import SwiftUI
import CodeScanner
struct ContentView: View {
#State var isPresentingScanner = false
#State var scannedCode: String = "Scan a QR code to get started."
var scannerSheet : some View {
CodeScannerView(
codeTypes: [.qr],
completion:{ result in
if case let.success(code) = result {
self.scannedCode = code.string
self.isPresentingScanner = false
}
}
)
} // ←
var body: some View {
VStack(spacing: 10) {
Text(scannedCode)
Button("Scan QR code") {
self.isPresentingScanner = true
}
.sheet(isPresented: $isPresentingScanner) {
self.scannerSheet
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
P.S. Something that would help you spot this type of error in the future would be to re-indent all the code.
You can do that by selecting all the code in the file (CMD+A), then selecting Editor -> Structure -> Re-Indent (CTRL+I)

How to link child View state to menus using FocusedValues

SwiftUI newbie here. 👋 I'm struggling to understand how #FocusedBinding and FocusedValues work when building menus for a MacOS app. I'm trying to build the Apple HIG UI pattern with buttons in the window toolbar for changing the list view, and matching menu items in the View menu. Much like Finder windows have the four different view modes.
I have gone through the Apple's Landmarks tutorial, the Frameworks Engineer's example code in Apple dev forum, and Majid's tutorial.
The Apple documentation says FocusedValues is "a collection of state exported by the focused view and its ancestors." I assume the collection is global and I can set a focusedValue in any child View, and read or bind to any of the FocusedValues from anywhere in my code.
Therefore I don't understand why my first example below works, but the second one doesn't?
This works:
import SwiftUI
#main
struct TestiApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.frame(minWidth: 200, minHeight: 300)
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}
.windowStyle(HiddenTitleBarWindowStyle())
.commands {
MenuCommands()
}
}
}
struct ContentView: View {
// In the working version of the code selectedView is defined here
// in the ContentView, which is a direct child of the WindowGroup
// that has the .commands modifier.
#State private var selectedView: Int = 0
// For demonstration purposes I have simplified the authorization
// code to a hardcoded boolean.
private var isAuthorized: Bool = true
var body: some View {
switch isAuthorized {
case true:
// focusedValue is set here to the selectedView binding.
// I don't really understand why do it here, but it works.
AuthorizedView(selectedView: $selectedView)
.focusedValue(\.selectedViewBinding, $selectedView)
default:
NotYetAuthorizedView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AuthorizedView: View {
// selectedView is passed to this view as an argument and
// bound from ContentView.
#Binding var selectedView: Int
var body: some View {
List {
Text("Something here")
Text("Something more")
Text("Even more")
}
.toolbar {
// The Picker element sets and gets the bound selectedView value
Picker("View", selection: $selectedView) {
Text("View 1").tag(0)
Text("View 2").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
struct NotYetAuthorizedView: View {
var body: some View {
VStack {
Text("You need permission to access this 😭")
.padding()
}
}
}
struct MenuCommands: Commands {
private struct MenuContent: View {
// Command menu binds the selectedView value through focusedValues.
// MenuContent is a View, because otherwise the binding doesn't
// work (I read there's a bug in SwiftUI...).
#FocusedBinding(\.selectedViewBinding) var selectedView: Int?
var body: some View {
Button("View 1") {
selectedView = 0
}
.keyboardShortcut("1")
Button("View 2") {
selectedView = 1
}
.keyboardShortcut("2")
}
}
var body: some Commands {
CommandGroup(before: .toolbar) {
MenuContent()
}
}
}
struct SelectedViewBinding: FocusedValueKey {
typealias Value = Binding<Int>
}
extension FocusedValues {
var selectedViewBinding: SelectedViewBinding.Value? {
get { self[SelectedViewBinding.self] }
set { self[SelectedViewBinding.self] = newValue }
}
}
But if I make the following changes to ContentView and AuthorizedView, the project compiles fine but the binding between selectedView and the command menus no longer works:
struct ContentView: View {
// selectedView definition has been removed from ContentView
// and moved to AuthorizedView.
private var isAuthorized: Bool = true
var body: some View {
switch isAuthorized {
case true:
AuthorizedView()
// Also setting the focusedValue here has been removed
default:
NotYetAuthorizedView()
}
}
}
struct AuthorizedView: View {
// Moved selectedView definition here
#State private var selectedView: Int = 0
var body: some View {
List {
Text("Something here")
Text("Something more")
Text("Even more")
}
.toolbar {
Picker("View", selection: $selectedView) {
Text("View 1").tag(0)
Text("View 2").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.focusedValue(\.selectedViewBinding, $selectedView)
// I am now setting the focusedValue here, which seems
// more logical to me...
}
}
}
I like the second example better, because the selectedView state is encapsulated in the AuthorizedView where it belongs.

Iterating an array on an #EnvironmentObject when the application closes on macOS

I have an #EnvironmentObject that serves an array to my main view. it's declared as follow:
my_app.swift
#main
struct My_AppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DataModel())
}
}
}
ContentView.swift
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var changed: Bool = false
}
final class DataModel: ObservableObject {
#AppStorage("mytestapp") public var notes: [NoteItem] = []
init() {
self.notes = self.notes.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
I call this from the different views in the ContentView.swift as:
struct AllText: View {
#EnvironmentObject private var data: DataModel
}
I added to my_app.swift th ability to detect when the user closes the app so I can perform some action.
#if os(macOS)
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationWillTerminate(_ aNotification: Notification) {
// trying to iterate on the struct within DataModel() here
print("app closing")
}
}
#endif
#main
struct My_AppApp: App {
#if os(macOS)
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DataModel())
}
}
}
And now, I'm trying to access the struct within DataModel() so I can check if each element has a changed set but no matter what I try, or how I declare the environmentObject I get a segfault, or errors such as No ObservableObject of type DataModel found. A View.environmentObjectfor DataModel may be missing as an ancestor of this view.
How can I access that DataModel and iterate thru it so I can perform an action when I close the app?
Here is possible approach - to inject data model on ContentView appear, like
#if os(macOS)
class AppDelegate: NSObject, NSApplicationDelegate {
var dataModel: DataModel? // << here
func applicationWillTerminate(_ aNotification: Notification) {
print("app closing")
// use self.dataModel? here
}
}
#endif
#main
struct My_AppApp: App {
#if os(macOS)
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif
private let dataModel = DataModel() // << here !!
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel) // << here !!
.onAppear {
#if os(macOS)
appDelegate.dataModel = self.dataModel // << here !!
#endif
}
}
}
}

Display subview from an array in SwiftUI

I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.

How to access a variable of an instance of a view in ContentView SwiftUI?

So in ContentView, I've created a view with the following:
ViewName()
I'd like to change a variable in ContentView to the value of a variable in ViewName. I was hoping I could do something like:
ViewName() {
contentViewVariable = self.variableNameInViewNameInstance
}
but that was just kind of a guess as to how to access the value; it didn't work. Any suggestions would be greatly appreciated!
You can use #State and #Binding to achieve that. You should watch these WWDC videos in 2019 to learn more about this.
wwdc 2019 204 - introduction to swiftui
wwdc 2019 216 - swiftui essentials
wwdc 2019 226 - data flow through swiftui
struct ContentView: View {
#State private var variable: String
var body: some View {
ViewName($variable)
}
}
struct ViewName: View {
#Binding var variableInViewName: String
init(variable: Binding<String>) {
_variableInViewName = variable
}
doSomething() {
// updates variableInViewName and also variable in ContentView
self.variableInViewName = newValue
}
}
For whatever reason it is needed technically it is possible to do via callback closure.
Caution: the action in such callback should not lead to refresh sender view, otherwise it would be just either cycle or value lost
Here is a demo of usage & solution. Tested with Xcode 11.4 / iOS 13.4
ViewName { sender in
print("Use value: \(sender.vm.text)")
}
and
struct ViewName: View {
#ObservedObject var vm = ViewNameViewModel()
var callback: ((ViewName) -> Void)? = nil // << declare
var body: some View {
HStack {
TextField("Enter:", text: $vm.text)
}.onReceive(vm.$text) { _ in
if let callback = self.callback {
callback(self) // << demo of usage
}
}
}
}
class ViewNameViewModel: ObservableObject {
#Published var text: String = ""
}

Resources