SwiftUI polymorphic behaviour not working for View - swift

protocol BackgroundContent: View{
}
struct BlueDivider: BackgroundContent {
var body: some View {
Divider()
.frame(minHeight: 1)
.background(.blue)
}
}
struct RedDivider: BackgroundContent {
var body: some View {
Divider()
.frame(minHeight: 1)
.background(.red)
}
}
var p: BackgroundContent = BlueDivider()
// Use of protocol 'BackgroundContent' as a type must be written 'any BackgroundContent'
p = RedDivider()
This always ask me to use
var p: any BackgroundContent = BlueDivider()
Is there any way to use generic type which accept any kind view?
Actually, I want to use view as a state like #State private var bgView: BackgroundContent = BlueDivider() which i want to change at runtime like bgView = RedDivider()
I have made my custome view to place some other view at runtime by using this state.

For your specific problem you can do something like this here:
struct SwiftUIView: View {
#State var isRed = false
var body: some View {
Devider()
.frame(height: 1)
.background(isRed ? Color.red : Color.blue)
}
}

It is complicated but i have found a solution of this problem. First thing i have done with ObservableObject. Here is my example.
protocol BaseBackgroundContent {
var color: Color { get set }
}
class BlueContent: BaseBackgroundContent {
var color: Color = .blue
}
class RedContent: BaseBackgroundContent {
var color: Color = .red
}
And i created a custom view for Divider in this case.
struct CustomDivider: View {
var backgroundContent: any BaseBackgroundContent
var body: some View {
Divider()
.background(backgroundContent.color)
}
}
And now i used a viewModel which can be observable, and the protocol has to be Published.
class ExampleViewModel: ObservableObject {
#Published var backgroundContent: any BaseBackgroundContent = RedContent()
func change() {
backgroundContent = BlueContent()
}
}
Final step is the view. This is a exampleView. If you click the button you will see the BlueContent which was RedContent
struct Example: View {
#ObservedObject var viewModel = ExampleViewModel()
init() {
}
var body: some View {
VStack {
Text("Test")
CustomDivider(backgroundContent: viewModel.backgroundContent)
Button("Change") {
viewModel.change()
}
}
}
}

Related

SwiftUI: UserDefaults Binding

I have a settings view that has a button which toggles a binding that's stored with UserDefaults.
struct Settings: View {
#ObservedObject var settingsVM = SetttingsViewModel()
var body: some View {
if settingsVM.settingActivated {
Text("Setting activated")
} else {
Text("Setting deactivated")
}
Button("Activate") {
settingsVM.settingActivated.toggle()
}
}
}
SettingsViewModel
class SetttingsViewModel: ObservableObject {
#Published var settingActivated: Bool = UserDefaults.standard.bool(forKey: "settingActivated") {
didSet {
UserDefaults.standard.set(self.settingActivated, forKey: "settingActivated")
}
}
}
The text("Setting activated/ Setting deactivated")in the Settings view update instantly when i press the button but the text in ContentView doesn't change unless i restart the app & i have no idea why.
struct ContentView: View {
#ObservedObject var settingsVM = SetttingsViewModel()
#State private var showsettings = false
var body: some View {
if settingsVM.settingActivated {
Text("Setting Activated")
.padding(.top)
} else {
Text("Setting Deactivated")
.padding(.top)
}
Button("Show Settings") {
showsettings.toggle()
}
.sheet(isPresented: $showsettings) {
Settings()
}
.frame(width: 300, height: 300)
}
}
This is for a macOS 10.15 app so i can't use #AppStorage
Right now, you don't have any code in you view model to react to a change in UserDefaults. Meaning, if UserDefaults gets a new value set, it won't know about it. And, since you're using a different instance of SettingsViewModel in your two different views, they can easily become out-of-sync.
The easiest change would be to pass the same instance of SettingsViewModel to Settings:
struct Settings: View {
#ObservedObject var settingsVM: SettingsViewModel //<-- Here
var body: some View {
if settingsVM.settingActivated {
Text("Setting activated")
} else {
Text("Setting deactivated")
}
Button("Activate") {
settingsVM.settingActivated.toggle()
}
}
}
struct ContentView: View {
#ObservedObject var settingsVM = SetttingsViewModel()
#State private var showsettings = false
var body: some View {
if settingsVM.settingActivated {
Text("Setting Activated")
.padding(.top)
} else {
Text("Setting Deactivated")
.padding(.top)
}
Button("Show Settings") {
showsettings.toggle()
}
.sheet(isPresented: $showsettings) {
Settings(settingsVM: settingsVM) //<-- Here
}
.frame(width: 300, height: 300)
}
}
Another option would be to use a custom property wrapper (like AppStorage, but available to earlier targets): https://xavierlowmiller.github.io/blog/2020/09/04/iOS-13-AppStorage
Also, #vadian's comment is important -- if you had access to it, you'd want to use #StateObject. But, since you don't, it's important to store your ObservableObject at the top level so it doesn't get recreated.

How to pass a FocusState variable to other views and allow those views to update the variable? [Swift]

Currently, I have something like the following:
struct ViewA: View {
#FocusState private var focusedField: Bool
var body: some View {
ViewB(focusedField: $focusedField)
// some other views that read focusedField...
}
}
struct ViewB: View {
#State var focusedField: FocusState<Bool>.Binding
var body: some View {
Button(action: {
focusedField = true // ERROR: Cannot assign value of type 'Bool' to type 'FocusState<Bool>'
})
// ...
}
}
Seems like I can pass down focusedField with no problem but unable to update its value. How to solve this?
For consistency, maybe it's better to use the property wrapper #FocusedState.Binding instead.
struct ViewA: View {
#FocusState private var focusedField: Bool
var body: some View {
ViewB(focusedField: $focusedField)
// some other views that read focusedField...
}
}
struct ViewB: View {
#FocusState.Binding var focusedField: Bool
var body: some View {
Button(action: {
focusedField = true
}) {
Text("Tap me")
}
}
}
This allows to do focusedField = true instead of focusedField.wrappedValue = true
Instead of directly setting focusedField, use the wrappedValue property.
struct ViewA: View {
#FocusState private var focusedField: Bool
var body: some View {
ViewB(focusedField: $focusedField)
// some other views that read focusedField...
}
}
struct ViewB: View {
var focusedField: FocusState<Bool>.Binding
var body: some View {
Button(action: {
focusedField.wrappedValue = true /// assign `wrappedValue`
}) {
Text("Click me!") /// Side note: you need a label for the button too, otherwise your code won't compile
}
// ...
}
}

How to correctly handle Picker in Update Views (SwiftUI)

I'm quite new to SwiftUI and I'm wondering how I should use a picker in an update view correctly.
At the moment I have a form and load the data in with .onAppear(). That works fine but when I try to pick something and go back to the update view the .onAppear() gets called again and I loose the picked value.
In the code it looks like this:
import SwiftUI
struct MaterialUpdateView: View {
// Bindings
#State var material: Material
// Form Values
#State var selectedUnit = ""
var body: some View {
VStack(){
List() {
Section(header: Text("MATERIAL")){
// Picker for the Unit
Picker(selection: $selectedUnit, label: Text("Einheit")) {
ForEach(API().units) { unit in
Text("\(unit.name)").tag(unit.name)
}
}
}
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Does anyone has experience with that problem or am I doing something terribly wrong?
You need to create a custom binding which we will implement in another subview. This subview will be initialised with the binding vars selectedUnit and material
First, make your MaterialUpdateView:
struct MaterialUpdateView: View {
// Bindings
#State var material : Material
// Form Values
#State var selectedUnit = ""
var body: some View {
NavigationView {
VStack(){
List() {
Section(header: Text("MATERIAL")) {
MaterialPickerView(selectedUnit: $selectedUnit, material: $material)
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Then, below, add your MaterialPickerView, as shown:
Disclaimer: You need to be able to access your API() from here, so move it or add it in this view. As I have seen that you are re-instanciating it everytime, maybe it is better that you store its instance with let api = API() and then refer to it with api, and even pass it to this view as such!
struct MaterialPickerView: View {
#Binding var selectedUnit: String
#Binding var material : Material
#State var idx: Int = 0
var body: some View {
let binding = Binding<Int>(
get: { self.idx },
set: {
self.idx = $0
self.selectedUnit = API().units[self.idx].name
self.material.unit = self.selectedUnit
})
return Picker(selection: binding, label: Text("Einheit")) {
ForEach(API().units.indices) { i in
Text(API().units[i].name).tag(API().units[i].name)
}
}
}
}
That should do,let me know if it works!

How can I use function type?

I want to know if there is a way to use a function Type as var, for example in this code I am trying to send a function to get triggered in a Button tap Action. Is this kind of programming even possible in Swift?
The down code is not working and It is for SwiftUI, but the question is applicable to Swift as well.
struct ContentView: View {
#State var backgroundColor: Color = Color.white
func backgroundColorFunction() { backgroundColor = Color.red }
var body: some View {
ZStack {
backgroundColor.ignoresSafeArea()
CustomView(incomingFuction: backgroundColorFunction()) // :Here
}
}
}
struct CustomView: View {
var incomingFuction: ???funcType??? = ???funcType???()
var body: some View {
Button("update Background Color") {
incomingFuction()
}
}
}
Yes, it is possible, moreover it is widely used, especially in SwiftUI.
Here is your updated code:
struct FuncContentView: View {
#State var backgroundColor: Color = Color.white
func backgroundColorFunction() { backgroundColor = Color.red }
var body: some View {
ZStack {
backgroundColor.ignoresSafeArea()
CustomView(incomingFuction: backgroundColorFunction) // << here !!
}
}
}
struct CustomView: View {
var incomingFuction: () -> () // << here !!
var body: some View {
Button("update Background Color") {
incomingFuction()
}
}
}

Pass view as parameter to Button triggering it as a Modal

I'd like to have a custom button struct that receives a view as a parameter that will be shown as modal when the button is clicked. However, the view parameter is always empty, and I can't find the mistake I'm doing. My button struct looks like that:
struct InfoButton<Content:View>: View {
#State private var showingInfoPage: Bool
private var infoPage: Content
init(infoPage: Content, showingInfoPage: Bool) {
self.infoPage = infoPage
_showingInfoPage = State(initialValue: showingInfoPage)
}
var body: some View {
return
Button(action: {
self.showingInfoPage.toggle()
}) {
Image(systemName: "info.circle")
.resizable()
.frame(width: 25, height: 25)
.foregroundColor(.white)
.padding()
}.sheet(isPresented: self.$showingInfoPage) {
self.infoPage
}.frame(minWidth: 0, maxWidth: .infinity, alignment: .topTrailing)
}
}
This button is placed in a navigation bar from a template I'm creating for multiple other views.
I think the most relevant parts of that template are these:
protocol TrainingView {
var title: String { get }
var subheadline: String { get }
var asAnyView: AnyView { get }
var hasInfoPage: Bool { get }
var infoPage: AnyView { get }
}
extension TrainingView where Self: View {
var asAnyView: AnyView {
AnyView(self)
}
var hasInfoPage: Bool {
false
}
var infoPage: AnyView {
AnyView(EmptyView())
}
}
struct TrainingViewTemplate: View {
#State var showInfoPage: Bool = false
#State var viewIndex: Int = 0
var body: some View {
//the views that conform to the template
let views: [TrainingView] = [
ExerciseView(),
TrainingSessionSummaryView()
]
return NavigationView {
ViewIterator(views, self.$viewIndex) { exerciseView in
VStack {
VStack {
Text(exerciseView.title)
.font(.title)
.fontWeight(.semibold).zIndex(1)
Text(exerciseView.subheadline)
.font(.subheadline)
Spacer()
exerciseView.asAnyView.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}.navigationBarItems(trailing: (exerciseView.hasInfoPage == true ? InfoButton(infoPage: exerciseView.infoPage, showingInfoPage: self.showInfoPage) : nil))
}
}
}
I debugged to the point, where the navigationBarItems are initialized. At that point, the exercise view has content for "hasInfoPage" and "infoPage" itself.
One exemplary Exercise View has a header like that:
struct ExerciseView: View, TrainingView {
var title: String = "Strength Session"
var subheadline: String = "Pushups"
var numberOfExercise: Int = 1
#State var ratingValue: Double = 0
#Environment(\.presentationMode) var presentationMode
var hasInfoPage: Bool = true
var infoPage = ExerciseDetailView()
So in this view, the infoPage gets initialized with the ExercieDetailView() which I receive in the TemplateView, but as soon as the InfoButton is clicked, the debugger shows an empty infoPage, even though the "showingInfoPage" variable contains the right value.
You don't confirm to protocol, so default infoPage from extension TrainingView is shown.
The solution is
struct ExerciseView: View, TrainingView {
// .. other code here
var infoPage = AnyView(ExerciseDetailView()) // << here !!
``