Reputation: 1385
Since SwiftUI and its ability to factorize methods into brackets (thanks to function builder) like so:
struct ContentView : View {
var body: some View {
VStack {
Text("Hello World!")
Text("by Purple Giraffe").color(.gray)
}
}
The function builder code is just here to emphasis the fact that factorizing can be handy. I don't expect it to help me doing factorized assignation.
I was wondering if it was possible to factorize assignations (w/ something else) into brackets like so:
struct AnimationViewConfiguration {
var contentMode:Int = 0
var mainTitle:String = "test"
var subTitle:String = ""
var alternativeSubtitle:String = ""
var numberOfIteration:Int = 0
var frameRate = 40
var maximumSimultaneousParalax:Int = 5
var minimumSimultaneousParalax:Int = 2
}
class someViewController: UIViewController {
var mainBackgroundAnimationViewConfig = AnimationViewConfiguration()
func animateBackground(_ useAlternativeBackground:Bool) {
// The normal bulky way
if useAlternativeBackground == false {
mainBackgroundAnimationViewConfig.contentMode = 3
mainBackgroundAnimationViewConfig.mainTitle = "Your super animation"
mainBackgroundAnimationViewConfig.subTitle = "A subtitle anyway"
mainBackgroundAnimationViewConfig.alternativeSubtitle = "Hey another one!"
// partial or complete assignation
// mainBackgroundAnimationViewConfig.numberOfIteration = 4
mainBackgroundAnimationViewConfig.frameRate = 40
mainBackgroundAnimationViewConfig.maximumSimultaneousParalax = 19
mainBackgroundAnimationViewConfig.minimumSimultaneousParalax = 3
} else {
// The way I'd like it to be
mainBackgroundAnimationViewConfig with { // imaginary `with` keyword
contentMode = 0
mainTitle = "Your super animation"
subTitle = "A subtitle anyway"
alternativeSubtitle = "Hey another one!"
// partial or complete assignation
// numberOfIteration = 4
frameRate = 40
maximumSimultaneousParalax = 19
minimumSimultaneousParalax = 3
}
}
}
}
The whole point would be to avoid repeating long variable name 15 times, knowing that you often do this with already 2,3,4 indenting (which makes it even more annoying to grab an eye on).
For people proposing to put it in a specific function, I'd say that for the same reasons we sometimes use anonymous functions (i.e. used only once...), it would still be convieniant to do the assignation without making more boilerplate too.
Thanks @matt for mentioning the with
keyword used for this purpose in other languages~
If it doesn't exist, is it comming in swift5.1+?
Wouldn't you find it handy?
Upvotes: 1
Views: 258
Reputation: 336
I've had the same problem myself, that's how I found your question (sorry if you've moved on since then).
I first thought SwiftUI would suggest an answer, but all it seems to do for assignments is chain methods together: Text.color(:)
from your example (actually it didn't work so I had to change it into Text.foregroundColor(:)
) is a method that returns a new Text instance with the new color, and color
may not even be a property of the struct as it seems to be backed by an undocumented modifiers
array that contains SwiftUI.Text.Modifier.color(Optional(gray))
after running your example.
So your strict requirements got me thinking, and in the end I found a rather simple workaround which involves adding a single (public) change(:)
method to your struct:
mutating func change(_ closure: (inout AnimationViewConfiguration) -> Void) {
closure(&self)
}
This will allow the following syntax, which I believe is quite close to our requirements:
var mainBackgroundAnimationViewConfig = AnimationViewConfiguration()
mainBackgroundAnimationViewConfig.change {
$0.contentMode = 0
$0.mainTitle = "Your super animation"
$0.subTitle = "A subtitle anyway"
$0.alternativeSubtitle = "Hey another one!"
// partial or complete assignation
// numberOfIteration = 4
$0.frameRate = 40
$0.maximumSimultaneousParalax = 19
$0.minimumSimultaneousParalax = 3
}
Unless I'm missing something, this should work… Sure, you can run arbitrary code in there, and you will need to explicitly refer to self
at the caller site if needed; and there may be some caveats with retain cycles one needs to look out for, but at first glance, I don't see anything beyond standard closure caveats. But please do point out any issues you may find!
Obviously, this version only works with mutable properties: trying to change a let
constant won't compile, whether the struct itself is the constant, or any of its properties. For the former case, you can return a modified copy instead:
func copyWithChanges(_ closure: (inout AnimationViewConfiguration) -> Void) -> AnimationViewConfiguration {
var copy = self
closure(©)
return copy
}
// call it like this:
let newVersionOfTheConfig = mainBackgroundAnimationViewConfig.copyWithChanges {
$0.contentMode = 12
// etc.
}
For classes, whether they're vars or constants, it's even simpler (if we're still assuming that all of their properties are mutable) since the closure will get a reference to the instance that it can modify at will:
func change(_ closure: (AnimationViewConfiguration2) -> Void) {
closure(self)
}
EDIT: As of Swift 5.2, you can name the change(_:)
function callAsFunction(_:)
and save some typing on the call site:
// inside the struct declaration:
mutating func callAsFunction(_ closure: (inout AnimationViewConfiguration) -> Void) {
closure(&self)
}
// ...
// using the struct:
var mainBackgroundAnimationViewConfig = AnimationViewConfiguration()
mainBackgroundAnimationViewConfig {
$0.contentMode = 0
$0.mainTitle = "Your super animation"
$0.subTitle = "A subtitle anyway"
$0.alternativeSubtitle = "Hey another one!"
}
End edit
However, if you'd want to use this syntax for initializing an instance, or for making a copy with new values for any of its immutable properties, I honestly don't see any way of accomplishing that, even if we tried to re-implement immutability with "delayed initialization" property wrappers (example use case in the proposal for the feature) but that seems to be out of scope: we only ever mentioned already-initialized instances and mutable properties here.
Upvotes: 1
Reputation: 535850
wouldn't you find it handy?
No. What’s wrong with
var test = Test(
a : 3,
c : 4,
s : "hey"
)
to start with? This leaves the other properties at their default values.
Or, if you mutate later,
test.c = 4
test.a = 3
test.s = "hey"
Or
(test.c, test.a, test.s) = (4, 3, "hey")
? I don’t see how another layer of syntactic sugar is desirable.
There are languages that use a with
construct that does the sort of thing you describe (distributing properties over a single reference instead of explicit dot notation every time), but I don’t yearn for it in Swift.
Edit after your edit: If it is merely the long name you object to, copy to a short-named temp variable, set the desired properties, and copy back:
var thisIsAReallyLongName = Whatever()
do {
var temp = thisIsAReallyLongName
temp.c = 4
temp.a = 3
temp.s = "hey"
thisIsAReallyLongName = temp
}
Upvotes: 3
Reputation: 17572
closest to what you want is
struct Test {
var a:Int = 0
var b:Int = 0
var s:String = ""
mutating func update(_ closure: (inout Int, inout Int, inout String)->Void)->Void {
closure(&a, &b, &s)
}
}
var test = Test()
test.update { (a, b, c) in
a = 10
b = 20
c = "Alfa"
}
or better (supporting partial update, and xcode gives you what is avaiable)
struct Test {
var a:Int = 0
var b:Int = 0
var s:String = ""
mutating func update(_ closure: (inout Self)->Void)->Void {
closure(&self)
}
}
var test = Test()
test.update { (v) in
v.a = 20
v.s = "Alfa"
}
UPDATE: who knows the future?
Swift New Features
You can call values of types that declare func callAsFunction methods like functions. The call syntax is shorthand for applying func callAsFunction methods.
struct Adder {
var base: Int
func callAsFunction(_ x: Int) -> Int {
return x + base
}
}
var adder = Adder(base: 3)
adder(10) // returns 13, same as
adder.callAsFunction(10)
You must include func callAsFunction argument labels at call sites. You can add multiple func callAsFunction methods on a single type, and you can mark them as mutating. func callAsFunction works with throws and rethrows, and with trailing closures. (59014791)
Upvotes: 1
Reputation: 7678
I think you're getting a bit confused. There's several concepts at play here and function builders don't play quite as nicely as you might think with the rest of it.
When code is placed between 2 braces, we call it a block of code. This is a good way to help organise our programs, because we can place related pieces of sequential logic together. The most common place for a block of code to appear is in a function. For example, I have a function that calculates a user's salary:
func calculateSalary() {
let monthly = 3500
let yearlyTotal = monthly * 12
print("Your salary is \(yearlyTotal)")
}
Then we can call the function like so:
calculateSalary()
But, we can also assign this function to a variable, where we refer to it as a closure. (Functions and closures are actually exactly the same thing, functions are just named closures that we can easily reuse and call).
let calculateSalary: () -> Void = {
let monthly = 3500
let yearlyTotal = monthly * 12
print("Your salary is \(yearlyTotal)")
}
As you would expect, we can call the function in the exact same way.
This means we can pass closures as parameters to methods or initialisers, so that another function or initialiser can call our function that we pass in. For example:
/**
* this is a stupid, pointless class (but it demonstrates
* closures-as-parameters nicely)
*/
class SalaryCalculator {
let closure: () -> Void
init(closure: () -> Void) {
self.closure = closure
}
}
let calculator = SalaryCalculator(closure: {
let monthly = 3500
let yearlyTotal = monthly * 12
print("Your salary is \(yearlyTotal)")
})
// call the closure!
calculator.closure()
This can also be written using trailing closure syntax, where we can remove the argument label if the closure is the last parameter to our initialiser (or function):
let calculator = SalaryCalculator {
let monthly = 3500
let yearlyTotal = monthly * 12
print("Your salary is \(yearlyTotal)")
}
// call the closure!
calculator.closure()
This is what you are seeing when the code appears to be encapsulated in braces (factorize is not a term that is used to describe this). It's actually a closure parameter being passed to the initialiser of the underlying type.
Function builders in SwiftUI use this same principal, but it's a bit more subtle and complex than I'd like to get into right now.
So, to answer your question more directly–if you'd like to break your code up into more manageable pieces, I'd create a class
or struct
that encapsulates some logic, then break up the sequentially related calls into functions on that new class
or struct
. I'm not sure why you would want to be doing assignments in the manner shown in your question, it doesn't really make sense especially with Swift's initialisers providing so much customisability anyway.
Upvotes: 0