Pass Binding To ViewModifier And Modify It - swiftui

SwiftUI:
This code is giving me so many headaches, it works without the Switch-Case statement
struct MyModifier: ViewModifier {
#Binding var animal: Animal
init(chosenAnimal: Binding<Animal>) {
self._animal = chosenAnimal
}
func body(content: Content) -> some View {
content
.offset(x: scrollOffset + dragOffset, y: 0)
..... CODE THAT HELPS ME MAKING A SNAPPABLE HSTACK IS HERE... NOT IMPORTANT
switch index {
case 4:
self._animal = Animal.Dog
case 3:
$animal = Animal.Cat
}
})
)
}
}
Those are the images that show the errors
I have tried:
adding mutating in func body(content: Content) -> some View but then it gives me an error saying I am not conforming ViewModifier.

Assign to property directly, like
case 4:
self.animal = Animal.Dog
case 3:
self.animal = Animal.Cat

Related

SwiftUI switch function without AnyView

I learned that I shouldn't be using AnyView() in SwiftUI. I have written many functions with switch statements to return corresponding Views like the below code. I'm trying to learn the best way to migrate the switch statements away from AnyView()
struct T01: View{
var body: some View{
VStack{
showView(i: 1)
}
}
func showView(i: Int) -> some View{
switch(i){
case 0: return AnyView(viewOne)
case 1: return AnyView(viewTwo)
default: return AnyView(EmptyView())
}
}
var viewOne: some View {
Text("View One")
}
var viewTwo: some View {
Text("View Two")
}
}
Thanks for any help!!!
Your showView function has to return the same type of view each time, which is why AnyView works.
You have a couple of options to fix this:
Wrap everything in another View, e.g. a Group
func showView(i: Int) -> some View {
Group {
switch i {
case 0:
viewOne
case 1:
viewTwo
default:
EmptyView()
}
}
}
Mark the function with the #ViewBuilder attribute
#ViewBuilder
func showView(i: Int) -> some View {
switch i {
case 0:
viewOne
case 1:
viewTwo
default:
EmptyView()
}
}

SwiftUI 4: NavigationSplitView behaves strangely?

In SwiftUI 4, there is now a NavigationSplitView. I played around with it and detected some strange behaviour.
Consider the following code: When the content function returns the plain Text, then there is the expected behaviour - tapping a menu item changes the detail view to the related text.
However, when commenting out the first four cases, and commenting in the next four, then a tap on "Edit Profile" does not change the detail view display. (Using #ViewBuilder does not change this behaviour.)
Any ideas out there about the reasons for that? From my point of view, this may just be a simple bug, but perhaps there are things to be considered that are not documented yet?!
struct MainScreen: View {
#State private var menuItems = MenuItem.menuItems
#State private var menuItemSelection: MenuItem?
var body: some View {
NavigationSplitView {
List(menuItems, selection: $menuItemSelection) { course in
Text(course.name).tag(course)
}
.navigationTitle("HappyFreelancer")
} detail: {
content(menuItemSelection)
}
.navigationSplitViewStyle(.balanced)
}
func content(_ selection: MenuItem?) -> some View {
switch selection {
case .editProfile:
return Text("Edit Profile")
case .evaluateProfile:
return Text("Evaluate Profile")
case .setupApplication:
return Text("Setup Application")
case .none:
return Text("none")
// case .editProfile:
// return AnyView(EditProfileScreen())
//
// case .evaluateProfile:
// return AnyView(Text("Evaluate Profile"))
//
// case .setupApplication:
// return AnyView(Text("Setup Application"))
//
// case .none:
// return AnyView(Text("none"))
}
}
}
struct MainScreen_Previews: PreviewProvider {
static var previews: some View {
MainScreen()
}
}
enum MenuItem: Int, Identifiable, Hashable, CaseIterable {
var id: Int { rawValue }
case editProfile
case evaluateProfile
case setupApplication
var name: String {
switch self {
case .editProfile: return "Edit Profile"
case .evaluateProfile: return "Evaluate Profile"
case .setupApplication: return "Setup Application"
}
}
}
extension MenuItem {
static var menuItems: [MenuItem] {
MenuItem.allCases
}
}
struct EditProfileScreen: View {
var body: some View {
Text("Edit Profile")
}
}
After playing around a bit in order to force SwiftUI to redraw the details view, I succeeded in this workaround:
Wrap the NavigationSplitView into a GeometryReader.
Apply an .id(id) modifier to the GeometryReader (e.g., as #State private var id: Int = 0)
In this case, any menu item selection leads to a redraw as expected.
However, Apple should fix the bug, which it is obviously.
I've found that wrapping the Sidebar list within its own view will fix this issue:
struct MainView: View {
#State var selection: SidebarItem? = .none
var body: some View {
NavigationSplitView {
Sidebar(selection: $selection)
} content: {
content(for: selection)
} detail: {
Text("Detail")
}
}
#ViewBuilder
func content(for item: SidebarItem?) -> some View {
switch item {
case .none:
Text("Select an Item in the Sidebar")
case .a:
Text("A")
case .b:
Text("B")
}
}
}

Building navigation between Views in my iOS app

As the time flies, by App get more and more complicated shape.
In some cases the App flow might be:
View A -> View B -> C -> D
and then back
D -> C -> B -> A..
but sometimes i need to skip the view and go
D -> B -> A..
In some cases its A -> C -> D and then D -> A
I started to use NavigationView/NavigationLink and in some cases i use the following approach:
let weekView = WeekView(journey: journey, isRoot: isRoot).environmentObject(self.thisSession)
window?.rootViewController = UIHostingController(rootView: weekView)
No i realize that it has become a complete mess.. It's time for me to rethink this..
How do you handle navigation in apps where it can't be always done by pushing/popping the views from Navigation stack?
Using ViewBuilders is a good option here.
#ViewBuilder func myViewRouter(selection: Selection) -> some View {
switch selection {
case selection1:
View1()
case selection2:
View2()
case selection3:
View3()
}
}
enum Selection { ... }
ViewBuilders are powerful, its pretty much a function that can return opaque types but notice the lack of the return keyword. This seems like a perfect use case for it. In the example I used a enum but its also common to see this used with a var selection = 0 on the parent view and have the ViewBuilder as the child. Either way, same functionality.
Below is is a good url to understand ViewBuilders.
https://swiftwithmajid.com/2019/12/18/the-power-of-viewbuilder-in-swiftui/
Last edit: Here is an example use case:
import SwiftUI
struct ContentView: View {
#State private var selection: SelectionEnum = .zero
var body: some View {
VStack {
showMyViews(selection: selection)
HStack {
ForEach(SelectionEnum.allCases, id: \.self) { selection in
Button(action: {self.selection = selection}){
Text(selection.rawValue)
.fontWeight(.bold)
.frame(width: 60, height: 60)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
}
}
}
}
}
#ViewBuilder func showMyViews(selection: SelectionEnum) -> some View {
switch selection {
case .zero:
ViewA()
case .one:
ViewB()
case .two:
ViewC()
case .three:
ViewD()
}
}
enum SelectionEnum: String, CaseIterable {
case zero
case one
case two
case three
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ViewA: View {
var body: some View {
Text("View A")
}
}
struct ViewB: View {
var body: some View {
Text("View B")
}
}
struct ViewC: View {
var body: some View {
Text("View C")
}
}
struct ViewD: View {
var body: some View {
Text("View D")
}
}

How to implement a color scheme switch with the system value option?

I have implemented a dark/light mode switch in my app using the guide here on this thread. Sample code below:
public struct DarkModeViewModifier: ViewModifier {
#AppStorage("isDarkMode") var isDarkMode: Bool = true
public func body(content: Content) -> some View {
content
.environment(\.colorScheme, isDarkMode ? .dark : .light)
.preferredColorScheme(isDarkMode ? .dark : .light) // tint on status bar
}
}
And to call it:
Picker("Color", selection: $isDarkMode) {
Text("Light").tag(false)
Text("Dark").tag(true)
}
.pickerStyle(SegmentedPickerStyle())
How to implement this with an addition of a System segment? I thought of setting an Int as a default setting, but I cannot figure out how to tie it with the #AppStorage property wrapper.
And also how does watching system mode changes come into effect here in SwiftUI?
Update: In iOS 15, it looks like windows is deprecated. How to update it for iOS 15 in the most sane way? I've seen some other solutions for isKeyWindow, but not sure how to apply it here.
To accomplish this, you will need to store the user's display preference from a Bool to a custom enum. Then, from this custom enum, you can determine whether the appearance should be dark or light, and apply the display preferences based on that.
Sample code:
struct ContentView: View {
enum DisplayMode: Int {
case system = 0
case dark = 1
case light = 2
}
#AppStorage("displayMode") var displayMode: DisplayMode = .system
func overrideDisplayMode() {
var userInterfaceStyle: UIUserInterfaceStyle
switch displayMode {
case .dark: userInterfaceStyle = .dark
case .light: userInterfaceStyle = .light
case .system: userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
}
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = userInterfaceStyle
}
var body: some View {
VStack {
Picker("Color", selection: $displayMode) {
Text("System").tag(DisplayMode.system)
Text("Light").tag(DisplayMode.light)
Text("Dark").tag(DisplayMode.dark)
}
.pickerStyle(SegmentedPickerStyle())
.onReceive([self.displayMode].publisher.first()) { _ in
overrideDisplayMode()
}
}.onAppear(perform: overrideDisplayMode)
}
}
Basically, what you are doing is
assigning each display mode an integer value (so it can be stored in #AppStorage)
setting up the picker to choose between system, dark, and light, and saving the value in UserDefaults
determining whether the app is in dark mode, by switching on the #AppStorage value
passing the custom dark mode configuration through the views and subviews by using UIApplication.shared.windows.first?.overrideInterfaceStyle
Thanks to #diogo for his solution. I have adapted it for ios 15 into a custom view which could be used in a settings page:
struct DisplayModeSetting: View {
enum DisplayMode: Int {
case system, dark, light
var colorScheme: ColorScheme? {
switch self {
case .system: return nil
case .dark: return ColorScheme.dark
case .light: return ColorScheme.light
}
}
func setAppDisplayMode() {
var userInterfaceStyle: UIUserInterfaceStyle
switch self {
case .system: userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
case .dark: userInterfaceStyle = .dark
case .light: userInterfaceStyle = .light
}
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
scene?.keyWindow?.overrideUserInterfaceStyle = userInterfaceStyle
}
}
#AppStorage("displayMode") var displayMode = DisplayMode.system
var body: some View {
HStack {
Text("Display mode:")
Picker("Is Dark?", selection: $displayMode) {
Text("System").tag(DisplayMode.system)
Text("Dark").tag(DisplayMode.dark)
Text("Light").tag(DisplayMode.light)
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: displayMode) { newValue in
print(displayMode)
displayMode.setAppDisplayMode()
}
}
}
}

Change view without Navigation link SwiftUI

I everyone! I spent hours looking for something that I guess very simple but I can not managed to find the best way...
I have my body view :
var body: some View {
VStack {
// The CircularMenu
CircularMenu(menuItems: homeMenuItems, menuRadius: 55, menuButtonSize: 55, menuButtonColor: .black, buttonClickCompletion: buttonClickHandler(_:))
.buttonStyle(PlainButtonStyle())
}
}
Which contains a circular menu. Each click on a menu item calls :
func buttonClickHandler(_ index: Int) {
/// Your actions here
switch index {
//Thermometer
case 0:
print("0")
//Light
case 1:
print("1")
//Video
case 2:
print("2")
//Alarm
case 3:
print("3")
//Car
case 4:
self.destinationViewType = .car
self.nextView(destination: .car)
default:
print("not found")
}
}
I want to perform a simple view transition to another view called Car. nextView function looks like this :
func nextView(destination: DestinationViewType) {
switch destination {
case .car: Car()
}
}
I thought that was simple like this but I get : Result of 'Car' initializer is unused on the case line.
So someone knows how to achieve that ? Thanks a lot in advance!
Here's one way to do it:
Create a struct called IdentifiableView which contains an AnyView and an id:
struct IdentifiableView: Identifiable {
let view: AnyView
let id = UUID()
}
Create a #State var to hold the nextView. Use .fullScreenCover(item:) to display the nextView
#State private var nextView: IdentifiableView? = nil
var body: some View {
VStack {
// The CircularMenu
CircularMenu(menuItems: homeMenuItems, menuRadius: 55, menuButtonSize: 55, menuButtonColor: .black, buttonClickCompletion: buttonClickHandler(_:))
.buttonStyle(PlainButtonStyle())
}.fullScreenCover(item: self.$nextView, onDismiss: { nextView = nil}) { view in
view.view
}
}
Then, assign self.nextView the IdentifiableView:
case .car: self.nextView = IdentifiableView(view: AnyView(Car()))
When it's time to return to the MenuView use self.presentationMode.wrappedValue.dismiss() to dismiss the view. Here is an example of a minimal Car view:
struct Car: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Text("Car View").onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
}
}
If you want to completely replace the body content with the new view, you need some condition about that. Let's say you have a Container with a body and if there is a Car view created we will display it:
struct Container: View {
#State var car: Car? // Storage for optional Car view
var body: some View {
if let car = car { // if the car view exists
car // returning the car view
} else { // otherwise returning the circular menu
VStack {
CircularMenu(menuItems: homeMenuItems, menuRadius: 55, menuButtonSize: 55, menuButtonColor: .black, buttonClickCompletion: buttonClickHandler(_:))
.buttonStyle(PlainButtonStyle())
}
}
}
...
And then we only need to assign the newly created instance of the car view on click:
...
func buttonClickHandler(_ index: Int) {
switch index {
....
//Car
case 4:
car = Car() // assigning the newly created instance
...
}
}
}
I see that you have mentioning of destinationViewTyp and some other cases. So your code will be slightly more complex than this, but the idea keeps the same. We store either a view or some information that helps us create a view when necessary and then returning either a picker or a view depending on condition.