SwiftUI : Display preview in canvas with conditional layout - swiftui

I'm building a layout with some conditional controls, depending on user preference.
For example, I'm displaying a Picker (segmented) only if the user has chosen to see it (user sets a Bool to true in UserDefaults on the settings page of the app).
I tried setting that Bool as a var or constant in my preview struct, and I managed to have my Picker displayed in the canvas, but I get an error : "Argument passed to call that takes no arguments" :
Here's how I declare my Bool :
private var isBiqualif: Bool = UserDefaults.standard.bool(forKey: kbiQualif) // kbiQualif is constant string set in a separate file to avoid typos
Here's how I display my Picker :
if isBiqualif { // User has activated this in settings
Picker("", selection: $typeSelectorIndex) {
ForEach(0 ..< types.count) { index in
Text(self.types[index]).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
Divider()
} // End of if statement for dual rating Picker
And my latest preview attempt :
struct CreateTakeoffView_Previews: PreviewProvider {
static var previews: some View {
let myBool = true
return CreateTakeoffView(isBiqualif: myBool)
}
}
Any ideas what's going on here ?
Thanks

Don't make it private, such you hide var, so instead use
var isBiqualif: Bool = UserDefaults.standard.bool(forKey: kbiQualif)

Related

SwiftUI view get views in the body or define view as property

I am trying to get the view inside the body for observing purpose, but looking for different ways the view inside the body can be accessed.
struct ContentView: View {
#State var userNameText: String
var body: some View {
startObservingInput()
return TextField("hello", text: $userNameText)
}
func startObservingInput() {
// How do we get TextField instance here.
// Option 1 - Pass as parmeter here.
// Option 2 - Is there a way to get view from body ex: self.body.textFieldView
// Option 3 - can create as a property in CotentView but the text binding refers to self which will not be allowed before its initalized so that will fail to compile
//var textField = TextField("hello", text: $userNameText)
}
}
Option 1 is simple, where we pass the TextField view.
Option 2 is something I am looking for, if we can get any view inside the hierarchy. In this case Text Field.
Option 3, Tried to create a property but I get the following error.
ex:
struct ContentView: View {
#State var userNameText: String
var textField = TextField("hello", text: $userNameText)
......
}
Cannot use instance member '$userNameText' within property initializer; property initializers run before 'self' is available
SwiftUI is different from what you're probably used to. Unlike UIKit, you don't "store" views in properties. There's no delegates either.
Instead, you directly pass in a property — userNameText — that will be linked to the text field's text. Since this updates itself automatically, you can use the onChange modifier to observe changes.
struct ContentView: View {
#State var userNameText: String
var body: some View {
TextField("hello", text: $userNameText)
.onChange(of: userNameText) { newValue in
print("Text changed to: \(newValue)")
}
}
}
Here is what I did, look at it:
//
// ViewProp.swift
// SwiftDemo1
//
// Created by CreoleMacbookPro on 12/19/22.
//
import SwiftUI
struct ViewProp: View {
#State var userNameText: String = " "
var body: some View {
let textField: TextField<Text> = TextField("hello", text: $userNameText)
let simpleText: Text = Text("Hello, World!")
let _ = print(type(of: textField))
startObservingInput(textField: textField)
Button {
userNameText = "Jatin Bhuva"
} label: {
Text("Press Me..")
}
// textField
}
func startObservingInput(textField: TextField<Text>) -> some View {
textField
// How do we get TextField instance here.
// Option 1 - Pass as parmeter here.
// Option 2 - Is there a way to get view from body ex: self.body.textFieldView
// Option 3 - can create as a property in CotentView but the text binding refers to self which will not be allowed before its initalized so that will fail to compile
//var textField = TextField("hello", text: $userNameText)
}
}
struct ViewProp_Previews: PreviewProvider {
static var previews: some View {
ViewProp()
}
}

SwiftUI clean up ContentView

I'm trying to simplify the ContentView within a project and I'm struggling to understand how to move #State based logic into its own file and have ContentView adapt to any changes. Currently I have dynamic views that display themselves based on #Binding actions which I'm passing the $binding down the view hierarchy to have buttons toggle the bool values.
Here's my current attempt. I'm not sure how in SwiftUI to change the view state of SheetPresenter from a nested view without passing the $binding all the way down the view stack. Ideally I'd like it to look like ContentView.overlay(sheetPresenter($isOpen, $present).
Also, I'm learning SwiftUI so if this isn't the best approach please provide guidance.
class SheetPresenter: ObservableObject {
#Published var present: Present = .none
#State var isOpen: Bool = false
enum Present {
case none, login, register
}
#ViewBuilder
func makeView(with presenter: Present) -> some View {
switch presenter {
case .none:
EmptyView()
case .login:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
LoginScreen()
}
case .register:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
RegisterScreen()
}
}
}
}
if you don't want to pass $binding all the way down the view you can create a StateObject variable in the top view and pass it with .environmentObject(). and access it from any view with EnvironmentObject
struct testApp: App {
#StateObject var s1: sViewModel = sViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(s1)
}
}
}
You are correct this is not the best approach, however it is a common mistake. In SwiftUI we actually use #State for transient data owned by the view. This means using a value type like a struct, not classes. This is explained at 4:18 in Data Essentials in SwiftUI from WWDC 2020.
EditorConfig can maintain invariants on its properties and be tested
independently. And because EditorConfig is a value type, any change to
a property of EditorConfig, like its progress, is visible as a change
to EditorConfig itself.
struct EditorConfig {
var isEditorPresented = false
var note = ""
var progress: Double = 0
mutating func present(initialProgress: Double) {
progress = initialProgress
note = ""
isEditorPresented = true
}
}
struct BookView: View {
#State private var editorConfig = EditorConfig()
func presentEditor() { editorConfig.present(…) }
var body: some View {
…
Button(action: presentEditor) { … }
…
}
}
Then you just use $editorConfig.isEditorPresented as the boolean binding in .sheet or .overlay.
Worth also taking a look at sheet(item:onDismiss:content:) which makes it much simpler to show an item because no boolean is required it uses an optional #State which you can set to nil to dismiss.

How to reload the TabView in order to change TabBar background color without having User actions on it (eg: from a settings panel change)?

To change TabBar background and other properties you have about 2 ways to process.
via the init() of the view containing the TabView
(see: https://stackoverflow.com/a/56971573/2192483)
via an .onAppear() directly on the TabView
(see: https://stackoverflow.com/a/63414605/2192483 - thanks to #Asperi ;
see: https://schwiftyui.com/swiftui/customizing-your-tabviews-bar-in-swiftui/ - thanks to #Dan O'Leary)
These solutions are efficients at View load and if you reload Tab Bar by touching tabs.
But IT DOES'NT WORK, if you want to change a TabBar properties programmatically, without User interaction on the TabBar Buttons, through the regular way of properties defined in #Published values. This doesn't work because either though init() or .onAppear(), these 2 methods are not subscribers of prop. publishers so the view embedding the TabView doesn't reload.
I finally found a way to trigger the TabView reload not from user tab button touches, with regular #Published properties.
Here is example based on an app with a settings panel allowing to change app skin, including the TabBar background color.
1) The Main struct embed the ContentView that embed the TabView
• SettingsModel embed the properties that share a) the skin values b) a flag to trigger the reload
• ContentView owns one param: skinChanged
#main
struct MyApp: App {
#ObservedObject private var settings = SettingsModel.shared
var body: some Scene {
WindowGroup {
ContentView(skinChanged: settings.skinChanged)
}
}
}
.
2) The ContentView struct embed the TabView
• init(skinChanged: Bool) allows to custom TabBar appearance that
will reload if settings.skinChanged changes of value
• init / if !skinChanged { } applies TabBar appearance only if needed
• body / if !self.settings.skinChanged { } allows to create a change in
the view alternating from TabView to a Rectangle (as a background)
struct ContentView: View {
#ObservedObject private var settings = SettingsModel.shared
init(skinChanged: Bool) {
if !skinChanged {
let tabBarAppearance = UITabBar.appearance()
tabBarAppearance.backgroundColor = UIColor(settings.skin.tabBarBg)
tabBarAppearance.unselectedItemTintColor = UIColor(settings.skin.tabBarUnselected)
}
}
var body: some View {
if !self.settings.skinChanged {
TabView(selection: $someStateProp) {
...
}
} else {
Rectangle()
.fill(settings.skin.tabBarBg)
}
}
}
.
3) The SettingModel struct publishes properties that trigger skin changes and skin values
• In order to create a change in ContentView(skinChanged:) with
settings.skinChanged, the value must pass from false to true then
back to false
• To let the UI applies each changes the 2 changes must be
parallelized with 2 synchronized tasks
class SettingsModel: ObservableObject {
static public let shared = SettingsModel()
...
#Published var skin = Skin(style: .modern)
#Published var skinId = 0 {
didSet {
...
self.skin = Skin(style: Skin.Style.allCases[self.skinId])
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.skinChanged = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.skinChanged = false
}
}
}
#Published var skinChanged = false
...
}

Odd first run behavior, possible Swift timing issue?

I'm finishing up online auditing of Stanford CS193P class (great class BTW) and I have an oddity on my last assignment. I have created a theme data store and I use it to select a theme (color, number of pairs of cards, emoji) and then kick off and play a matching game. That works fine. Using an edit button, the user can edit a theme and change any of the theme elements.
I run into a problem the first time I use the edit button and select a theme to edit. My code acts as if the #State myEditTheme is nil. If I force unwrap it it crashes. I have put it in a nil-coalescing option as shown, the edit window comes up with the first theme in the array. Any subsequent edit attempts work normally.
In the tap gesture function, I set the value of the #State var myEditTheme, then I set the themeEditing to true. My debug print statement indicates that the myEditTheme has been properly set. When the sheet(isPresented: $themeEditing) presents the ThemeEditor in a "sheet" view, the initial value of myEditTheme is nil.
Is there a timing issue between when I set it in the tap function and when Swift senses that themeEditing is true? The code below is obviously not functional as is, I have edited it for conciseness, only showing relevant portions.
struct ThemeManager: View {
#EnvironmentObject var store: ThemeStore // injected
#State private var editMode: EditMode = .inactive
// inject a binding to List and Edit button
#State private var myEditTheme: Theme?
#State private var themeEditing = false
// used to control .sheet presentation for theme editing
var body: some View {
NavigationView {
List {
ForEach(store.themes) { theme in
NavigationLink(destination: ContentView(viewModel: EmojiMemoryGame(theme: theme))) {
VStack(alignment: .leading) {
Text(theme.name).font(.title2)
Text(theme.emojis).lineLimit(1)
} // end VStack
.sheet(isPresented: $themeEditing) {
ThemeEditor(theme: $store.themes[myEditTheme ?? theme])
.environmentObject(store)
}
.gesture(editMode == .active ? tap(theme) : nil)
} // end NavigationLink
} // end ForEach
} // end List
.navigationTitle("Themes")
.navigationBarTitleDisplayMode(.inline) // removes large title, leaves small inline one
.toolbar {
ToolbarItem { EditButton() }
ToolbarItem(placement: .navigationBarLeading) {
newThemeButton
}
}
.environment(\.editMode, $editMode)
} // NavigationView
} // body
private func tap(_ theme:Theme) -> some Gesture {
TapGesture().onEnded {
myEditTheme = theme
print("edit theme: \(myEditTheme)")
themeEditing = true
}
}

How to print() to Xcode console in SwiftUI?

So I tried to put a print statement while debugging in a SwiftUI View.
print("landmark: \(landmark)")
In the following body.
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Favorite only")
}
ForEach(landmarkData) { landmark in
print("landmark: \(landmark)")
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
Compiler errors out:
So, what is the proper way to print to console in SwiftUI?
EDIT:
I made Landmark conform to CustomStringConvertible:
struct Landmark: Hashable, Codable, Identifiable, CustomStringConvertible {
var description: String { name+"\(id)" }
var id: Int
var name: String
.....
I still get the "String is not convertible to any" error. Should it work now?
You can easily add a print statement anywhere in a function builder by simply storing its return value in a wildcard, effectively ignoring it:
let _ = print("hi!")
No setup or other verbosity needed!
Why does this work while a regular print() doesn't?
The way SwiftUI's #ViewBuilder (and result builders in general) is that they consume any values in a closure that aren't used otherwise (e.g. if you just have 42 on its own line). The print function returns Void (nothing), which the builder would have to build into a view, so it fails. By instead assigning it to a variable (in this case _, basically a variable that you can never access), the Void is never offered to the view builder in the first place.
You could argue the builder should simply accept and ignore Void values, but the idea is that your builder closures should not have side effects (I'd remove print statements after finishing debugging too)—you should not rely on these closures being called at certain times.
Here's a helper Print( ... ) View that acts like a print( ... ) function but within a View
Put this in any of your view files
extension View {
func Print(_ vars: Any...) -> some View {
for v in vars { print(v) }
return EmptyView()
}
}
and use inside of body like so
Print("Here I am", varOne, varTwo ...)
or inside a ForEach {} like so
self.Print("Inside ForEach", varOne, varTwo ...)
Note: you might need to put Print() into a Group {} when combining with existing views
Try right-clicking on the live preview play button and selecting 'Debug Preview from the popup
You can print in the body structure but to do so you have to explicitly return the view you want to render. The body property inside a View is just a computed property like any other in Swift that implicitly returns the view. And just like any other computed property, you can perform operations inside the computed property as long as a value is explicitly returned. For example, this will throw an error when you try to print because there is no explicit return:
struct SomeView: View {
#State var isOpen = false
var body: some View {
print(isOpen) // error thrown here
VStack {
// other view code
}
}
}
But if we explicitly return the view we want then it will work e.g.
struct SomeView: View {
#State var isOpen = false
var body: some View {
print(isOpen) // this ok because we explicitly returned the view below
// Notice the added 'return' below
return VStack {
// other view code
}
}
}
The above will work well if you're looking to view how state or environment objects are changing before returning your view, but if you want to print something deeper down within the view you are trying to return, then I would go with #Rok Krulec answer.
It is possible to use print() remembering that all SwiftUI View content are (a) implicit closures and (b) it is highly recommended to decompose views as much as possible to have simple structure, so it might look like the following...
struct Model: Identifiable {
let value: String
var id: String {
value
}
init (_ value: String) {
self.value = value
}
}
struct TestView: View {
#State var showFavoritesOnly = false
#State var listData: [Model] = [Model("one"), Model("two"), Model("three")]
var body: some View {
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Favorite only")
}
ForEach(listData) { data in
self.rowView(data: data)
}
}
}
}
private func rowView(data: Model) -> some View {
#if DEBUG
print(">> \(data.value)")
#endif
return NavigationLink(destination: Text("Details")) {
Text("Go next from \(data.value)")
}
}
}
... and right clicking in Preview to select run as Debug Preview we get:
2019-10-31 14:28:03.467635+0200 Test[65344:11155167] [Agent] Received connection, creating agent
2019-10-31 14:28:04.472314+0200 Test[65344:11155168] [Agent] Received display message
>> one
>> two
>> three
You can declare a printing() method that includes print() and returns EmptyView struct.
struct ContentView: View {
#State private var offset = CGSize.zero
func printing(_ items: Any...) -> some View {
let _ = print(items)
return EmptyView()
}
var body: some View {
#if DEBUG
printing(offset) // prints [(0.0, 0.0)]
#endif
ZStack {
Text("Hello")
}
}
}
The safest and easiest way to print while debugging in a SwiftUI View.
extension View {
func Print(_ item: Any) -> some View {
#if DEBUG
print(item)
#endif
return self
}
}
Usage Example:
struct ContentView: View {
var body: some View {
VStack {
ForEach((1...5), id: \.self) { number in
Text("\(number)")
.Print(number)
}
}
}
}
Console output:
1
2
3
4
5
It can be generalized to:
extension View {
func Perform(_ block: () -> Void) -> some View {
block()
return EmptyView()
}
}
So in your example:
ForEach(landmarkData) { landmark in
Perform { print("landmark: \(landmark)") }
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
Here you go. It will just work like simple print but inside a view.
func printv( _ data : Any)-> EmptyView{
print(data)
return EmptyView()
}
and use it like that
struct ContentView: View {
var body: some View {
VStack() {
Text("hello To SwiftUI")
printv("its easy to code in SwiftUI")
Text("And Good to have you here")
}
}
}
The following extension on View is as intuitive as print because it's made to replicate the default print(_:separator:terminator:) function signature & behavior.
extension View {
func printUI(_ args: Any..., separator: String = " ", terminator: String = "\n") -> EmptyView {
let output = args.map(String.init(describing:)).joined(separator: separator)
print(output, terminator: terminator)
return EmptyView()
}
}
Usage Example:
struct ContentView: View {
var body: some View {
VStack {
printUI("ContentView", "1")
printUI("ContentView", "2", separator: ", ", terminator: "\n.\n.\n")
printUI("ContentView", "3", separator: "; ")
Text("Hello, World!")
}
}
}
Console Output:
ContentView 1
ContentView, 2
.
.
ContentView; 3
EDIT: Debug Preview is no longer supported in the latest versions of Xcode.
Very easy way to debug your Preview:
Open your Swift project in Xcode 11.
Right-click (or Control-click) on the Live Preview button in the bottom right corner of the preview.
Select Debug Preview.
How to debug your SwiftUI previews in Xcode
// Try this, add a 'return' on a view then the 'print' can stay alive in.
struct ContentView: View {
var num: Int = 1
var body: some View {
print(num)
return Text("hello")
}
}
You can't because you're in a computed property. You need for example a button and in the action you define the print. Or work with breakpoints
You can not print in body structure i.e. a structure which is some view type.For print you need to make function out of body structure and call it using button or something else.
This should work
if true {
print(aVar, "xx")
}
return ZStack {
...
}