How to transition between two custom views based on state change? - animation

I have a state variable in an ObservedObject that determines which of two custom views I show in SwiftUI.
I've messed around with .animation(.easeIn) in various locations and tried things with .withAnimation(), but I can't get anything to happen besides XCode complaints while experimenting. Regardless of where I put .animation() I either get a compile error no nothing happens when I run the code. Just flick from one view to another when I trigger a state change.
struct EventEditorView : View { /* SwiftUI based View */
var eventEditorVC : EventEditorVC!
#ObservedObject var eventEditorDataModel: EventEditorDataModel
var body: some View {
switch( eventEditorDataModel.editMode) {
case .edit:
EventEditModeView(eventEditorVC: eventEditorVC, eventEditorDataModel: eventEditorDataModel)
case .view:
EventViewModeView(eventEditorVC: eventEditorVC, eventEditorDataModel: eventEditorDataModel)
}
}
}

You can use a .transition on your elements and withAnimation when you change the value that affects their state:
enum ViewToShow {
case one
case two
}
struct ContentView: View {
#State var viewToShow : ViewToShow = .one
var body: some View {
switch viewToShow {
case .one:
DetailView(title: "one", color: .red)
.transition(.opacity.combined(with: .move(edge: .leading)))
case .two:
DetailView(title: "two", color: .yellow)
.transition(.opacity.combined(with: .move(edge: .top)))
}
Button("Toggle") {
withAnimation {
viewToShow = viewToShow == .one ? .two : .one
}
}
}
}
struct DetailView : View {
var title: String
var color : Color
var body: some View {
Text(title)
.background(color)
}
}

Related

Swiftui TextEditor seems not to respond to state changes

I am having trouble getting the SwiftUI TextEditor to work when it is in a Child View.
This is a small example that demonstrates the issue for me:
import SwiftUI
struct ContentView: View {
#State private var someText: String = "Hello World"
var body: some View {
VStack {
HStack {
Button("Text 1", action: {someText = "hello"})
Button("Text 2", action: {someText = "world"})
}
ViewWithEditor(entry: $someText)
}
}
}
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
When I click the buttons I expected the Editor text to change, however it remains with its initial value.
The print statement show that the "localString" variable is being updated.
Is TextEditor broken or am I missing something fundamental ??
If you move the buttons into the same view as the TextEditor, directly changing local state var it works as expected.
This is being run under MacOS in case it makes a difference.
TIA Alan.
Ok. So a proxy Binding does the job for me. See updated editor view below:
struct ViewWithEditor: View {
#Binding var entry: String
var body: some View
{
let localString = Binding<String> (
get: {
entry
},
set: {
entry = $0
}
)
return VStack {
Text(entry)
TextEditor(text: localString)
}
}
A bit more ugly (proxy bindings just seem clutter), but in some ways simpler..
It allows for the result of the edit to be reviewed / rejected before being pushed into the bound var.
This is occurring because the binding entry var is not actually being used after initialization of ViewWithEditor. In order to make this work without using the proxy binding add onChange to the ViewWithEditor as below:
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
The problem here is that now entry is not updating if localString changes. One could just use the same approach as before:
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
.onChange(of: localString) {
entry = $0
}
}
but why not just use $entry as the binding string for TextEditor?

How to move focus to view which is not TextField

I have a MacOS app which has a lot of TextFields in many views; and one editor view which has to receive pressed keyboard shortcut, when cursor is above. But as I try, I cannot focus on a view which is not text enabled. I made a small app to show a problem:
#main
struct TestFocusApp: App {
var body: some Scene {
DocumentGroup(newDocument: TestFocusDocument()) { file in
ContentView(document: file.$document)
}
.commands {
CommandGroup(replacing: CommandGroupPlacement.textEditing) {
Button("Delete") {
deleteSelectedObject.send()
}
.keyboardShortcut(.delete, modifiers: [])
}
}
}
}
let deleteSelectedObject = PassthroughSubject<Void, Never>()
struct MysticView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.gray.opacity(0.3))
}.focusable()
.onReceive(deleteSelectedObject) { _ in
print ("received")
}
}
}
enum FocusableField {
case wordTwo
case view
case editor
case wordOne
}
struct ContentView: View {
#Binding var document: TestFocusDocument
#State var wordOne: String = ""
#State var wordTwo: String = ""
#FocusState var focus: FocusableField?
var body: some View {
VStack {
TextField("one", text: $wordOne)
.focused($focus, equals: .wordOne)
TextEditor(text: $document.text)
.focused($focus, equals: .editor)
///I want to receive DELETE in any way, in a MystickView or unfocus All another text views in App to not delete their contents
MysticView()
.focusable(true)
.focused($focus, equals: .view)
.onHover { inside in
focus = inside ? .view : nil
/// focus became ALWAYS nil, even set to `.view` here
}
.onTapGesture {
focus = .view
}
TextField("two", text: $wordTwo)
.focused($focus, equals: .wordTwo)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(document: .constant(TestFocusDocument()))
}
}
Only first TextField became focused when I click or hover over MysticView
I can assign nil to focus, but it will not unfocus fields from outside this one view.
Is it a bug, or I missed something? How to make View focusable? To Unfocus all textFields?

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.

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.

SwiftUI performance issue showing conditional views in Items in a ForEach loop embedded in aScrollView

Working with SwiftUI:
I have a list of views in a ScrollView that I am creating using a ForEach loop. I want to show or hide a number of little flags depending on 4 different Bool properties in the struct I am using as the model for the objects in the list. The problem I'm having is the more If-statements I add, the worse the performance gets. With no If-statements the list loads without a hitch. I'm running into this problem with only 120 items in the list. I would love help figuring out what I'm doing wrong!
Here is an example of the Content View with the ScrollView and loop:
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
ScrollView(.vertical, showsIndicators: true){
VStack(spacing: 10){
ForEach(model.list){ item in
ItemView(item: item)
}
}
}
}// end body
} // end struct
And the Item Views and Struct I'm using as the model.
struct ListData: Identifiable {
var id = UUID()
var title: String
var subtitle: String
var isTrue: Bool
var isAlsoTrue:Bool
}
struct ItemView: View {
var item: ListData
var body: some View {
VStack(alignment: .leading){
Text(item.title)
HStack{
if item.isTrue{
FlagView(text: "True")
}
if item.isAlsoTrue{
FlagView(text: "Also")
}
Text(item.subtitle)
Spacer()
}
.font(.system(size: 12, weight: .semibold))
}
.font(.system(size: 14, weight: .semibold))
}
}
struct FlagView: View {
var text: String
var body: some View {
Text(text)
.padding(2)
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 2).foregroundColor(.gray))
}
}
The Actual objects have 4 Bool properties I want to use. Is there a better way to hide or show these flags in my list? Any help figuring out what's wrong with the performance is greatly appreciated!

Resources