I would like to change the language in my app, in order to do that I want to use Buttons.
So for each language I will have a Button, which will pass a parameter with the language code of that language.
Now I know it is possible to pass a function to a Button but how to pass a function with a Parameter?
To pass parameters to the action, I think there are multiple ways to do so. But I found this the most elegant way of doing it.
First of all I create a function which accepts a parameter "language". Then that function will return an anonymous function which uses the "language" parameter:
func changeLanguage(language: String) -> () -> () {
return {
print(language)
}
}
In the view I can create Buttons which will hold the anonymous function which is being returned by "changeLanguage" with a "language" parameter that can differ per Button.
var body: some View {
List {
Section(header: Text(NSLocalizedString("Language", comment: "settings.view.select.language"))) {
Button(NSLocalizedString("English", comment: "settings.view.language.english"), action: self.changeLanguage(language: "en"))
Button(NSLocalizedString("Dutch", comment: "settings.view.language.dutch"), action: self.changeLanguage(language: "nl"))
}
}
.listStyle(GroupedListStyle())
}
You can use a closure calling a method with your parameter, like this:
import SwiftUI
struct LanguageView: View {
var body: some View {
Button(action: { selectLanguage(language: "en") }) {
Text("English")
}
}
func selectLanguage(language: String) {
print("Language selected: \(language)")
}
}
Related
I am trying to refactor the logic o a button so I created a buttonStyle with a ternary operator inside of itself, but I am getting two errors:
Type 'ButtonStyle' has no member 'bordered'
Type 'ButtonStyle' has no member 'borderedProminent'
this is my code:
struct SelectButton: View {
#Binding var isSelecting: Bool
var body: some View{
if( isSelecting){
Button(action: {
self.isSelecting.toggle()
}, label: {
Text(isSelecting ? "Selecting" : "Select")
})
.buttonStyle(isSelecting ? .borderedProminent : .bordered)
.clipShape(RoundedRectangle(cornerRadius: 25))
}
}
}
I do not know if a struct or func -> some View is the best way to refactor.
The reason why you can't use it's because they have Mismatch types
You can either have one or the other. One is of type BorderedProminentButtonStyle while the other is of type BorderedButtonStyle
The below code shows how can achieve your result without encapsulating the entire button in a if or creating another view.
You create a View extension (this way it works for any View) then you can apply properties conditionally.
So here's the View extension
extension View {
func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> TupleView<(Self?, Content?)> {
if conditional {
return TupleView((nil, content(self)))
} else {
return TupleView((self, nil))
}
}
}
The way to use it would be somewhat simple
Button(action: {
self.isSelecting.toggle()
}, label: {
Text(isSelecting ? "Selecting" : "Select")
})
.if(!isSelecting) { $0.buttonStyle(.bordered) }
.if(isSelecting) { $0.buttonStyle(.borderedProminent) }
.clipShape(RoundedRectangle(cornerRadius: 25))
I'm unsure if there's another way to compact this but I tested this on Playground and it works as expected, depending on your view it might or might not introduce bugs
I have 2 custom button styles and I want to change the style when I tap the button. I tried this way:
Button(action: {
self.pressed.toggle()
})
{
Text("Button")
}.buttonStyle(pressed ? style1() : style2())
But it is not working, it is giving me an error from the VStack that it belongs to:
Unable to infer complex closure return type; add explicit type to disambiguate
If I do something like:
.buttonStyle(style1())
Or
.buttonStyle(style2())
Then the error goes away, so it's not from style1() or style2().
It's swift type-checking violation... I would recommend instead
Button(action: {
self.pressed.toggle()
})
{
Text("Button")
}.buttonStyle(Your_style(condition: pressed)) // << put conditional styling inside
See for example solution in Change buttonStyle Modifier based on light or dark mode in SwiftUI
You can create a useful extension like below
extension View {
func conditionalModifier<M1: ViewModifier, M2: ViewModifier>
(on condition: Bool, trueCase: M1, falseCase: M2) -> some View {
Group {
if condition {
self.modifier(trueCase)
} else {
self.modifier(falseCase)
}
}
}
func conditionalModifier<M: ViewModifier>
(on condition: Bool, trueCase: M) -> some View {
Group {
if condition {
self.modifier(trueCase)
}
}
}
}
Usage;
#State var condition = false
var body: some View {
Text("conditional modifier")
// Apply style if condition is true, otherwise do nothing
.conditionalModifier(on: condition, trueCase: Style1())
// Decision between 2 style
.conditionalModifier(on: condition, trueCase: Style1(), falseCase: Style2())
}
I'm trying to create a field modifiedBy with type: Object (to Meteor users).
I see you can setup blackbox: true for a Custom Object, but if I want to setup to a specific Object say a Group (collection) field modifiedBy is the logged in user, any pointers/help is greatly appreciated.
Thanks
As far as I see it, you have two options:
Store user-ids there with type: String
Denormalize it as you proposed
Denormalize it as you proposed
To denormalize it you can do something like this inside your schema:
...
modifiedBy: {
type: object
}
'modifiedBy._id': {
type: String,
autoValue: function () {
return Meteor.userId()
}
}
'modifiedBy.username': {
type: String,
autoValue: function () {
return Meteor.user().username
}
}
...
As you pointed out, you'd want to update these properties when they change:
server-side
Meteor.users.find().observe({
changed: function (newDoc) {
var updateThese = SomeCollection.find({'modifiedBy.username': {$eq: newDoc._id}})
updateThese.forEach () {
SomeCollection.update(updateThis._id, {$set: {name: newDoc.profile.name}})
}
}
})
Store user-ids there with type: String
I'd recommend storing user-ids. It's cleaner but it doesn't perform as well as the other solution. Here's how you could do that:
...
modifiedBy: {
type: String
}
...
You could also easily write a Custom Validator for this. Now retrieving the Users is a bit more complicated. You could use a transform function to get the user objects.
SomeCollection = new Mongo.Collection('SomeCollection', {
transform: function (doc) {
doc.modifiedBy = Meteor.users.findOne(doc.modifiedBy)
return doc
}
})
But there's a catch: "Transforms are not applied for the callbacks of observeChanges or to cursors returned from publish functions."
This means that to retrieve the doc reactively you'll have to write an abstraction:
getSome = (function getSomeClosure (query) {
var allDep = new Tacker.Dependency
var allChanged = allDep.changed.bind(allDep)
SomeCollection.find(query).observe({
added: allChanged,
changed: allChanged,
removed: allChanged
})
return function getSome () {
allDep.depend()
return SomeCollection.find(query).fetch()
}
})
I try to get the returned data in my Template.rendered function.
The current code is:
this.route('editCat', {
layoutTemplate : 'layoutCol2Left',
template : 'modCategoriesEdit',
path : '/mod/categories/edit/:_id',
yieldTemplates : _.extend(defaultYieldTemplates, {
'navigationBackend' : {to : 'contentLeft'}
}),
waitOn : function () {
return Meteor.subscribe('oneCat', this.params._id);
},
data : function () {
return Categories.findOne({_id : this.params._id});
}
});
In this block i wait on the subscribtion of the Collection Document and return the Document as data.
Now i can use the returned Document in my Template like this:
<template name="modCategoriesEdit">
<h1>Edit {{name}}</h1>
</template>
My problem is that i have to use the returned data in my rendered function like this:
Template.modCategoriesEdit.rendered = function () {
console.log(this.data);
}
But this returns "null".
So my question is:
How is it possible to get access to the returned data in the rendered function ?
Solution:
Just add the following to your iron-router route() method.
action : function () {
if (this.ready()) {
this.render();
}
}
Than the Template will rendered after all is loaded correctly.
There are 3 solutions if you want to wait until the waitOn data is ready before rendering:
1- Add an action hook to each route
Router.map(function()
{
this.route('myRoute',
{
action: function()
{
if (this.ready())
this.render();
}
}
}
2- Use the onBeforeAction hook globally or on every route
Sample code for the global hook:
Router.onBeforeAction(function(pause)
{
if (!this.ready())
{
pause();
this.render('myLoading');
}
});
myLoading (or whatever name) must be a template you have defined somewhere.
Don't forget the this.render line, otherwise the problem will occur when leaving the route (while the original problem occurs when loading the route).
3- Use the built-in onBeforeAction('loading') hook
Router.configure(
{
loadingTemplate: 'myLoading',
});
Router.onBeforeAction('loading');
myLoading (or whatever name) must be a template you have defined somewhere.
Using the action hook to check for this.ready() works, but it looks like the official way to do this is to call the following:
Router.onBeforeAction("loading");
Reference: https://github.com/EventedMind/iron-router/issues/679
Like #Sean said, the right solution is to use a loading template:
Router.onBeforeAction("loading");
But if you don't want it, like me, I came up with this solution:
Template.xxx.rendered = function() {
var self = this;
this.autorun(function(a) {
var data = Template.currentData(self.view);
if(!data) return;
console.log("has data! do stuff...");
console.dir(data);
//...
});
}
Template.currentData is reactive, so in the first time it is null and in the second it has your data.
Hope it helps.
-- Tested on Meteor v1.0 with Iron Router v1.0
I have a Template named movies, that has a method that returns a list of objects from a collection.
The query to generate that list of objects is created dynamically using data from another template method.
I would like to re-render the template, or just the components associated with that specific template method, whenever the filter data changes.
Here are the two methods used:
Template.movies.filter = function () {
if (Session.equals("filter", undefined)) {
return {};
}
return Session.get("filter");
};
Template.movies.movies = function () {
return Movies.find(Template.movies.filter(), {sort: [["Poster", "desc"]]});
};
On the HTML side it's a simple {{#each movies}}{{> movie}}{{/each}} to show the results from the movies method.
The problem is that when Session.get("filter") changes and therefore so does Template.movies.filter(), the HTML component relying on Template.movies.movies() data won't be updated with the new query results.
How would I achieve that behavior?
The easiest way is to just make a javascript function that both helpers utilize:
var getFilter = function() {
if (Session.equals("filter", undefined)) {
return {};
}
return Session.get("filter")
}
Template.movies.filter = function() { return getFilter(); }
Template.movies.movies = function () {
return Movies.find(getFilter(), {sort: [["Poster", "desc"]]});
};
This will react as you expect.