英文:
SwiftUI: restrict extension to custom view struct / return custom view type
问题
我想要实现的目标:
CustomView()
.doSomething() // ← 仅在 CustomView 上可用
.doSomethingElse() // ← 仅在 CustomView 上可用
AnyOtherView()
.doSomething() // ← 不应该编译
就像 SwiftUI 的 Text 实现具有相同的功能:
我尝试过的:
struct CustomView: View {
...
}
extension CustomView {
func doSomething() -> some CustomView {
self.environment(\.someKey, someValue)
}
func doSomethingElse() -> some CustomView {
self.environment(\.someOtherKey, someOtherValue)
}
}
我收到以下错误:“不透明类型必须仅指定 'Any'、'AnyObject'、协议和/或基类”。
我还尝试过的:
extension CustomView {
func doSomething() -> CustomView {
self.environment(\.someKey, someValue)
}
func doSomethingElse() -> CustomView {
self.environment(\.someOtherKey, someOtherValue)
}
}
我收到以下错误:“无法将返回类型为 'some View' 的返回表达式转换为返回类型 'CustomView'”。
Xcode 提供了以下修复:
extension CustomView {
func doSomething -> CustomView {
self.environment(\.someKey, someValue) as! CustomView
}
}
但是强制转换并不是一个很好的解决方案。
如何解决这个问题?
我只想扩展 CustomView
。我不想扩展 View
并返回 some View
,因为这将使所有视图都暴露功能(这不是我想要的)。
如果我扩展 CustomView 并简单地返回 some View,那么我无法同时使用这两个函数。
编辑:为什么我想要实现这个?
我正在构建一个 Swift 包,为多个项目提供 CustomView
。
为了使 CustomView
易于使用,我希望通过视图修饰符来配置它,而不是简单的初始化程序。
我可以像这样使用我提供的 CustomView
:
CustomView(value1: someValue, value2: someOtherValue)
... 但我希望使其更像 SwiftUI,可以使用可选的视图修饰符,如下所示:
CustomView()
.value1(someValue)
.value2(someOtherValue)
如果需要在视图上使用其他视图修饰符,如 tint(...)
或 fixedSize()
,等等,那将会非常方便,就像您配置 Text
一样,您可以使用视图修饰符而不是初始化程序,因为自定义是可选的。
英文:
What I want to achieve:
CustomView()
.doSomething() // ← should only be available on CustomView
.doSomethingElse() // ← should only be available on CustomView
AnyOtherView()
.doSomething() // ← should not compile
Pretty much like SwiftUI's Text implementation has that exact functionality:
What I tried
struct CustomView: View {
...
}
extension CustomView {
func doSomething() -> some CustomView {
self.environment(\.someKey, someValue)
}
func doSomethingElse() -> some CustomView {
self.environment(\.someOtherKey, someOtherValue)
}
}
I get the following error: "An 'opaque' type must specify only 'Any', 'AnyObject', protocols, and/or a base class".
What I also tried:
extension CustomView {
func doSomething() -> CustomView {
self.environment(\.someKey, someValue)
}
func doSomethingElse() -> CustomView {
self.environment(\.someOtherKey, someOtherValue)
}
}
I get the following error: Cannot convert return expression of type 'some View' to return type 'CustomView'.
Xcode provides the following fix:
extension CustomView {
func doSomething -> CustomView {
self.environment(\.someKey, someValue) as! CustomView
}
}
But force casting does not really look like a great solution.
How can I fix this?
I only want to extend CustomView
. I don't want to extend View
and return some View
because that would expose functionality to all views (which is not what I want).
If I extend CustomView and simply return some View, then I cannot use both functions at the same time.
Edit: Why do I want to achieve that?
I am building up a Swift Package to provide CustomView
to multiple projects.
And to make CustomView
easy to use I wanted to make it configurable with view modifiers instead of a simple initializer.
I could use my provided CustomView
like that:
CustomView(value1: someValue, value2: someOtherValue)
... but I wanted to make it more SwiftUI-Like in the way of optional view modifiers like that:
CustomView()
.value1(someValue)
.value2(someOtherValue)
That would look nice if I needed other view modifiers on that view like tint(...)
or fixedSize()
, etc. Much like you would configure Text
, which you customize with view modifiers instead of the initializer, since customizing is optional.
答案1
得分: 2
正如其他人指出的,如果你的函数返回类型不是你的CustomView
,那么你就不能在函数中使用任何修改器。
有了这个说法,你可以这样做:
struct CustomView: View {
@State var myBackground = Color.clear
var body: some View {
Text("Hello World!")
.background(myBackground)
}
}
extension CustomView {
func customBackground(_ newBackground: Color) -> some CustomView {
self.myBackground = newBackground
return self
}
func clearBackground() -> some CustomView {
self.myBackground = .clear
return self
}
}
然后根据需要使用它:
struct AnotherView: View {
var body: some View {
VStack {
CustomView()
.customBackground(.blue)
.clearBackground()
.customBackground(.red)
// ... 等等
Text("Hi everyone!")
.customBackground(.blue) // <--- 这将导致编译错误
}
}
}
但是你不能使用任何擦除你类型的其他修改器。为了实现你期望的行为,你在函数/修改器中所做的所有更改必须只针对你的结构体直接可访问的属性进行,并且具有你确保是你视图类型的返回值。
英文:
As others have pointed out, you cannot pipe any modifier in your functions that have a return type other than your CustomView
With that said, you can do something like:
struct CustomView: View {
@State var myBackground = Color.clear
var body: some View {
Text("Hello World!")
.background(myBackground)
}
}
extension CustomView {
func customBackground(_ newBackground: Color) -> some CustomView {
self.myBackground = newBackground
return self
}
func clearBackground() -> some CustomView {
self.myBackground = .clear
return self
}
}
And use it as needed:
struct AnotherView: View {
var body: some View {
VStack {
CustomView()
.customBackground(.blue)
.clearBackground()
.customBackground(.red)
// ... and so on
Text("Hi everyone!")
.customBackground(.blue) // <--- this will fail compiling
}
}
}
But you cannot use any other modifier that erases your type. To reach your desired behavior, all the changes done in your functions/modifiers must be done only on properties accessible by your struct directly and have a return value that you guarantees is your view's type
答案2
得分: 1
你如何知道environment
返回的是CustomView
?实际上你并不知道 - environment
返回的类型是SwiftUI的实现细节。当你看到environment
返回的是类似ModifiedContent<CustomView, _EnvironmentKeyWritingModifier<T>>
(如Ashley Mills的回答中所示)时,你的整个前提不成立,是吗?
让我们内联doSomething
:
CustomView()
.environment(\.someKey, someValue)
.doSomethingElse()
在这里,你并不是在CustomView()
上调用doSomethingElse
- 你是在environment
的返回值上调用它,而environment
甚至不返回CustomView
,那么为什么doSomethingElse
应该可用呢?
重新表述你的需求的一种方式是追踪调用链的“根” - 一种说法是“这个方法应该在以CustomView
为根的调用链上可用”。
你可以通过从doSomething
返回一个包装器,该包装器也具有根的类型,并将doSomething
和doSomethingElse
添加到包装器类型中来实现这一点。但在我看来,这对于它的价值来说实在是太过复杂了。
protocol ViewChain: View {
associatedtype ChainRoot
}
struct
<details>
<summary>英文:</summary>
How do you know `environment` returns `CustomView`? You don't - what type it returns is an implementation detail of SwiftUI. Your whole premise falls apart when you see that `environment` returns something like `ModifiedContent<CustomView, _EnvironmentKeyWritingModifier<T>>` (as shown in Ashley Mills' answer), doesn't it?
Let's inline `doSomething`:
CustomView()
.environment(.someKey, someValue)
.doSomethingElse()
You are not calling `doSomethingElse` on `CustomView()` here - you are calling it on the return value of `environment`, and `environment` doesn't even return `CustomView`, so why should `doSomethingElse` be available?
One way to rephrase what you want is to keep track of the "root" of the call chain - a way to say "this method should be available on call chains that has `CustomView` as its root"
You can sort of do this by returning from `doSomething` a wrapper that also has the root's type, and adding the `doSomething` and `doSomethingElse` to also the wrapper types. But IMO this is really convoluted for what it's worth.
protocol ViewChain: View {
associatedtype ChainRoot
}
struct WrappedViewChain<Body: View, ChainRoot: View>: ViewChain {
let wrapped: Body
let rootType: ChainRoot.Type
var body: some View {
wrapped.body
}
}
extension CustomView: ViewChain {
typealias ChainRoot = CustomView
}
extension ViewChain where ChainRoot == CustomView {
func doSomething() -> WrappedViewChain<some View, CustomView> {
WrappedViewChain(wrapped: environment(.someKey, someValue), rootType: CustomView.self)
}
func doSomethingElse() -> WrappedViewChain<some View, CustomView> {
WrappedViewChain(wrapped: environment(\.someOtherKey, someOtherValue), rootType: CustomView.self)
}
}
This essentially made it so that `doSomething()` and `doSomethingElse` "remembers" the root of the chain in the second type parameter of `WrappedViewChain`.
Note that this still won't work if you make it "forget" the root somewhere along the way, but `Text` behaves like this too.
CustomView()
.frame(width: 100) // forgets the root
.doSomething() // now this doesn't work
CustomView()
.doSomething() // OK
.frame(width: 100) // forgets the root
.doSomethingElse() // now this doesn't work
</details>
# 答案3
**得分**: 0
这是翻译后的代码部分:
```swift
不是一个解决方案,恐怕只是一个解释。
问题,正如您已经发现的,是这个代码的结果:
self.environment(.someKey, someValue)
不是`CustomView`。该函数不会修改现有的视图,而是创建一个新视图。
假设`.someKey`是一个`Int`,那么您可以通过以下方式进行测试:
```swift
extension CustomView {
func doSomething() -> some View {
let newView = self.environment(\.someKey, 23)
print(type(of: newView))
return newView
}
}
然后您将看到实际返回的类型是:
ModifiedContent<CustomView, _EnvironmentKeyWritingModifier<Int>>
这意味着如果您还在CustomView
的扩展中声明了doSomethingElse()
,您将无法将其应用于doSomething()
的结果。
您也无法强制将doSomething()
的结果强制转换为CustomView
,因为它们只是不同的类型,所以会导致崩溃。
<details>
<summary>英文:</summary>
Not a solution, I'm afraid, but an explanation.
The problem, as you've discovered, is that the result of
self.environment(.someKey, someValue)
is *not* `CustomView`. The function doesn't modify the existing view, it creates a new one.
Let's say `.someKey` is an `Int`, then you can test this out as follows:
extension CustomView {
func doSomething() -> some View {
let newView = self.environment(.someKey, 23)
print(type(of: newView))
return newView
}
}
and you'll see that the actual returned type is
ModifiedContent<CustomView, _EnvironmentKeyWritingModifier<Int>>
This means that if you also declare `doSomethingElse()` in an extension on `CustomView` you won't be able to apply it to the result of `doSomething()`.
Nor can you force cast the result of `doSomething()` to `CustomView` as they are just different types, so it will crash.
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论