I'm writing an inspector view mocking macOS Sidebar behavior in SwiftUI. It use .transition(.move(edge: .trailing)) only when the whole view appear and disappear but doesn't when switching between inner views. Here's the demo.
So I come up with following code. Selection is an enum representing views one-to-one which always starts with case none. inspect() method is used for change selection value and determining if using animation (remember I mentioned not to use animation when switching between inner views). I attach my full code at the end.
However, there'll never be any animation applied. How can I fix this?
If I use this more specific one that uses #State selection directly in inspect() method, the animation comes back, but cannot reuse the code if I have another Inspector, like #State var selection2: Selection2.
func inspect(to target: Selection) {
if target.rawValue == 0 {
withAnimation {
selection = target
}
} else {
if selection.rawValue == 0 {
withAnimation {
selection = target
}
} else {
selection = target
}
}
}
Here comes my full code:
struct ContentView: View {
enum Selection: Int {
case none
case view1
case view2
case view3
}
#State var selection: Selection = .none
func inspect<Selection: RawRepresentable<Int>>(_ selection: inout Selection, to target: Selection) {
if target.rawValue == 0 {
withAnimation {
selection = target
}
} else {
if selection.rawValue == 0 {
withAnimation {
selection = target
}
} else {
selection = target
}
}
}
var body: some View {
HStack {
VStack {
Button("None") {
inspect(&selection, to: .none)
}
Button("View 1") {
inspect(&selection, to: .view1)
}
Button("View 2") {
inspect(&selection, to: .view2)
}
Button("View 3") {
inspect(&selection, to: .view3)
}
}
Group {
if selection.rawValue == 1 {
Text("Hello 1")
} else if selection.rawValue == 2 {
Text("Hello 2")
} else if selection.rawValue == 3 {
Text("Hello 3")
}
}
.transition(.move(edge: .trailing))
}
}
}
Your code looks a bit to complicated so I have simplified it and if I understood what you want to do correctly it also behaves as expected.
First I made use of the #State property directly in the inspect function thus removing the need to pass it as an argument and also generics is not needed here
func inspect(to target: Selection) {
if target == .none {
withAnimation {
selection = target
}
} else {
if selection == .none {
withAnimation {
selection = target
}
} else {
selection = target
}
}
}
As seen above I am using the enum cases directly and not any raw value, as a matter of fact I removed that from the enum
enum Selection {
case none
case view1
case view2
case view3
}
and also change the code in the Group because of this
Group {
if selection == .view1 {
Text("Hello 1")
} else if selection == .view2 {
Text("Hello 2")
} else if selection == .view3 {
Text("Hello 3")
}
}
Overall I think this makes the code cleaner and easier to read.
Related
An error occurs when the Add button is pressed. On ios15 it worked fine.
On ios 15 it worked fine. but on ios16
An error occurs when the Add button is touched. I don't know why. Help.
Error: Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1068e25ec)
public struct ListTest2: View
{
#State var data: [Int] = []
public var body: some View
{
ScrollViewReader
{ proxy in
VStack
{
Button {
proxy.scrollTo(data.count - 1)
} label: {
Text("goto end")
}
Button {
proxy.scrollTo(1)
} label: {
Text("goto start")
}
Button {
data.append(data.count + 1)
proxy.scrollTo(data.count - 1)
} label: {
Text("Add")
}
List {
ForEach(data, id:\.self)
{ index in
Text("\(index )")
}
}
}
.onAppear()
{
for index in 1...30 {
data.append(index)
}
}
}
}
}
So we know that this bug is in iOS 16 with the List, majorly it's affecting the indexes of the items and also causing problems with lazy loading (loading items when reached second last item) in my case, so the proper fix I figured without any hack is,
#ObservedObject private var friendsModel = FriendsModel.shared
ScrollView {
LazyVStack {
ForEach(friendsModel.friends.indices, id: \.self) { index in
Button {
// your action
} label: {
// your cell view
}
.onAppear {
if index == friendsModel.friends.count - 2 {
// load more items
}
}
}
if friendsModel.isLoading {
ProgressView()
.padding())
}
}
}
.refreshable {
friendsModel.refresh()
}
Use ScrollView with LazyVStack and you will still be able to use features like refreshable, etc.
Testing this piece of code with Xcode 14 beta 5. Everything is working properly (sorting items with an animation, plus saving the selected sortOrder in UserDefaults).
However the if-else condition in the Menu's label seems to be ignored and the label is not updated. Please do you know why? Or what is alternate solution to avoid this?
struct ContentView: View {
enum SortOrder: String, CaseIterable, Identifiable {
case forward
case reverse
var id: Self {
self
}
static let `default`: Self = .forward
var label: String {
switch self {
case .forward: return "Sort (forward)"
case .reverse: return "Sort (reverse)"
}
}
}
#AppStorage("sortOrder") var sortOrder: SortOrder = .default {
didSet {
sort()
}
}
#State private var items = ["foo", "bar", "baz"]
#ToolbarContentBuilder
var toolbar: some ToolbarContent {
ToolbarItem {
Menu {
ForEach(SortOrder.allCases) { sortOrder in
Button {
withAnimation { self.sortOrder = sortOrder }
} label: {
// FIXME: doesn't reflect sortOrder value
if sortOrder == self.sortOrder {
Label(sortOrder.label, systemImage: "checkmark")
} else {
Text(sortOrder.label)
}
}
}
} label: {
Label("Actions", systemImage: "ellipsis.circle")
}
}
}
var body: some View {
NavigationStack {
List(items, id: \.self) { item in
Text(item)
}
.navigationTitle("Test")
.toolbar { toolbar }
.onAppear(perform: sort)
}
}
func sort() {
switch sortOrder {
case .forward:
items.sort(by: <)
case .reverse:
items.sort(by: >)
}
}
}
It is selected but Menu is not updated, assuming in toolbar it should be persistent.
A possible workaround is to force-rebuild menu on sort change, like
Menu {
// ...
} label: {
Label("Actions", systemImage: "ellipsis.circle")
}
.id(self.sortOrder) // << here !!
Tested with Xcode 14b5 / iOS 16
The if-else in your ToolbarItem is working correctly. The problem here is that .labelStyle in a toolbar are defaulted to .titleOnly.
To display the label with both icon and title, you would need to add .labelStyle(.titleAndIcon).
The solution was simply to use a Picker with .pickerStyle modifier set to .menu, instead of a hardcoded Menu.
var toolbar: some ToolbarContent {
ToolbarItem {
Picker(selection: $selectedSort) {
ForEach(Sort.allCases) { sort in
Text(sort.localizedTitle).tag(sort)
}
} label: {
Label("Sort by", systemImage: "arrow.up.arrow.down")
}
.pickerStyle(.menu)
}
}
Now SwiftUI automatically updates the interface.
I have the following view:
struct ChordPadView: View {
[...]
init() {
[...]
}
var body: some View {
[...]
if globalState.interfaceMode == .Normal {
HStack {
[...]
SomeView(playChord, stopChord) {
VStack {
Text(showType ? "\(chord.note)\(chord.type)" : chord.note)
.font(Font.custom("AeroMaticsBold", size: 15))
if (!self.hideNumeral) {
Text(self.numeral ?? chord.numeral ?? "")
.font(Font.custom("AeroMaticsBold", size: 8))
}
}
}
}
} else {
SomeOtherView(playChord, stopChord) {
VStack {
Text(showType ? "\(chord.note)\(chord.type)" : chord.note)
.font(Font.custom("AeroMaticsBold", size: 15))
if (!self.hideNumeral) {
Text(self.numeral ?? chord.numeral ?? "")
.font(Font.custom("AeroMaticsBold", size: 8))
}
}
}
}
}
}
Basically, I have two very different view, SomeOtherView and SomeOtherView and within the logic of my view I need to pass to either one or the other the very same block of content.
How can I refactor my code in order to avoid the duplication and stay DRY? Is there a way to assign the VStack block to a variable to use it in multiple places?
Create a custom view for the VStack. Either use it directly in the if-else statement, or pass it to ChordPadView.
struct ChordPadView: View {
var body: some View {
if (true) {
HStack {
VStackView()
}
} else {
VStackView()
}
}
}
Or
struct ChordPadView<Content: View>: View {
var content: Content
init(content: Content) {
self.content = content
}
var body: some View {
if (true) {
HStack {
VStackView()
}
} else {
VStackView()
}
}
}
struct VStackView: View {
var body: some View {
VStack{
Text("VStack View")
}
}
}
The else statment is never executed since the condition is always true.
I have a form that I want to change views from if a Bool is false:
var body: some View {
NavigationView {
Form {
Section {
Toggle(isOn: $ClientAnswer) {
Text("Client answer")
}
if ClientAnswer {
Toggle(isOn: $submission.fieldOne ) {
Text("Field One")
}
Toggle(isOn: $submission.fieldTwo ) {
Text("Field Two")
}
}
}
Section {
Button(action: {
if self.ClientAnswer{
self.placeSubmission()
}
else {
ShowRS() //**change views here.**
print("test")
}
}){
Text("Submit")
}
}.disabled(!submission.isValid)
}
}
}
The code is being executed as print("test") works, but it doesn't change view it just stays on the same view?
The view I am trying to switch to is:
struct ShowRS: View {
var body: some View {
Image("testImage")
}
}
You have to include ShowRS in your view hierarchy -- right now, it's just in your Buttons action callback. There are multiple ways to achieve this, but here's one option:
struct ContentView : View {
#State private var moveToShowRSView = false
var body: some View {
NavigationView {
Button(action: {
if true {
moveToShowRSView = true
}
}) {
Text("Move")
}.overlay(NavigationLink(destination: ShowRS(), isActive: $moveToShowRSView, label: {
EmptyView()
}))
}
}
}
struct ShowRS: View {
var body: some View {
Image("testImage")
}
}
I've simplified this down from your example since I didn't have all of your code with your models, etc, but it demonstrates the concept. In the Button action, you test a boolean (in this case, it'll always return true) and then set the #State variable moveToShowRSView.
If moveToShowRSView is true, there's an overlay on the Button that has a NavigationLink (which is invisible because of the EmptyView) which will only be active if moveToShowRSView is true
Trying to following this discussion, I implemented the suggestion of Yurii Kotov:
struct ContentView: View {
#State private var index = 0
#ViewBuilder
var body: some View {
if index == 0 {
MainView()
} else {
LoginView()
}
}
It works fine. But if I try to use a switch statement instead:
switch index {
case 0: MainView()
case 1: LoginView()
default:
print("# error in switch")
}
nothing happens. There is no mistake alert, but also no result at all. Could somebody help?
I had an issue with the default case where I wanted a "break" type situation for the switch to be exhaustive. SwiftUI requires some type of view, I found that
EmptyView() solved the issue.
also noted here EmptyView Discussion that you "have to return something"
struct FigureListMenuItems: View {
var showContextType: ShowContextEnum
#Binding var isFavoriteSeries: Bool?
var body: some View {
Menu {
switch showContextType {
case .series:
Button(action: { toggleFavoriteSeries() }) {
isFavoriteSeries! ?
Label("Favorite Series?", systemImage: "star.fill")
.foregroundColor(.yellow)
:
Label("Favorite Series?", systemImage: "star")
.foregroundColor(.yellow)
}
default: // <-- can use EmptyView() in this default too if you want
Button(action: {}) {
Text("No menu items")
}
}
} label: {
switch showContextType {
case .series:
isFavoriteSeries! ?
Image(systemName: "star.fill")
.foregroundColor(.yellow)
:
Image(systemName: "star")
.foregroundColor(.yellow)
default:
EmptyView()
}
Label("Menu", systemImage: "ellipsis.circle")
}
}
private func toggleFavoriteSeries() {
isFavoriteSeries?.toggle()
}
}
Enum for switch
enum ShowContextEnum: Int {
case series = 1
case whatIsNew = 2
case allFigures = 3
}
As #Sweeper said: your if...else and switch...case statements are not equal. The main idea is: body is just a computed variable of a View protocol and it should return something of its' type. Sure, you can do it with switch...case statements. In your code snippet mistake is in default statement: body cannot return() -> Void, only some View. So your code should looks like this:
struct ViewWithSwitchStatements: View {
#State var option = 0
var body: some View {
switch option {
case 1:
return AnyView(Text("Option 1"))
case 2:
return AnyView(Text("Option 2"))
default:
return AnyView(Text("Wrong option!"))
}
}
}
However you can't put it into VStack or something else, like if...else statements.