How can I conform to ShapeStyle protocol in SwiftUI? - swiftui

I am looking to conform to ShapeStyle, the goal is just conforming to ShapeStyle(Nothing more and nothing less).
Here is my code and the issue:
struct MyContentView: View {
var body: some View {
Circle()
.fill(RedView())
}
}
struct RedView: View {
var body: some View {
Color.red
}
}
So I know I could use Color.red directly instead of a view, but as I said, the goal of this question is conformation to ShapeStyle. For those going to share apple developer link about ShapeStyle, I have to say I was there before.
Error:
Instance method 'fill(_:style:)' requires that 'RedView' conform to 'ShapeStyle'

Disclaimer: below is only for demo, because all used API are private (by convention, not by design), so reaction of Apple if you submit app with such code to AppStore is unpredictable.
Note: as used interfaces are private they can be changed with any new version.
Main part:
public func _apply(to shape: inout SwiftUI._ShapeStyle_Shape) {
Color.red._apply(to: &shape) // << here !!
}
Which gives on Xcode 13.3 / iOS 15.4
Complete findings and code is here

If you want any kind of View to conform to ShapeStyle, then #Asperi provided here the answer.
If you want a Color to conform to ShapeStyle like in your example, it already adopts that protocol, so you just need a little tweak to make it work, like this:
struct RedView {
var render: some ShapeStyle {
Color.red
}
}
struct MyContentView: View {
var body: some View {
Circle()
.fill(RedView().render)
}
}

From the documentation for ShapeStyle:
You don’t use the ShapeStyle protocol directly. Instead, use one of the concrete styles that SwiftUI defines. To indicate a specific color or pattern, you can use Color or the style returned by image(_:sourceRect:scale:), or one of the gradient types, like the one returned by radialGradient(_:center:startRadius:endRadius:). To set a color that’s appropriate for a given context on a given platform, use one of the semantic styles, like background or primary.
I believe what you're trying to do can be achieved with:
struct MyContentView: View {
var body: some View {
Circle()
.fill(.red)
}
}

Related

SwiftUI: how can I hide scroll indicators? [duplicate]

I want to create a SwiftUI List, but not show scroll indicators. ScrollView offers showsIndicators to do this. How can it be done?
Any Indicators (List, scrollView, etc.)
you can get rid of showing indicators for all Lists, but with an API of the UITableView. because SwiftUI List is using UITableView for iOS behind the scene:
struct ContentView: View {
init() {
UITableView.appearance().showsVerticalScrollIndicator = false
}
var body: some View {
List(0...100, id: \.self) { item in
Text("hey")
}
}
}
Note that this will eliminate all TableViews and Lists indicators. You should make it visible again if you need to.
⚠️ Not Yet Important Note
Seems like Apple is removing appearance hacks (but not for this one yet). So you can use LazyVStack inside and ScrollView instead of List and use the available argument for hiding the indicators.
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) { // <- This argument
LazyVStack {
ForEach(1...100, id: \.self) {
Text("\($0)").frame(height: 40)
}
}
}
}
}
It is actually easy to accomplish this without any appearance work arounds in the answer accepted. You just have to use the ScrollView initializer and set the parameter showsIndicators to false.
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}
Inside your ScrollView you could use LazyVStack, if you have a lot of subviews to scroll through. SwiftUI will then render very efficiently your subviews: "lazy" -> only if needed).
Until there is a native SwiftUI way to achieve this, the Introspect library provides a decent solution.
After applying all modifiers to your list just add as a last modifier the following:
List {
...
}
.introspectTableView { tableView in
tableView.showsVerticalScrollIndicator = false // here you can access any other UITableView property
}
I hope there is a native way to do that at some point.
Hide scrolling indicator now became very simple
List {}.scrollIndicators(ScrollIndicatorVisibility.hidden)
List basically creates a tableview (UpdateCoalescingTableView) behind the scenes, and tableview's are scrollable. Unfortunately, however, you can't get to the scrollview attributes in SwiftUI.
You "might" be able to create a UIViewRepresentable that could walk up the view hierarchy until it finds a scrollview, but I wouldn't recommend it.
You could also create your own scrollview, put a vstack inside it, and "fake" a list view, which would probably be the safer approach.
The choosen answer won't work in iOS 16. They released a new viewModifier called .scrollIndicators(.hidden). I created a viewModifier wrapper which you can call like this on your List: .modifier(HideListIndicatorsViewModifier())
struct HideListIndicatorsViewModifier: ViewModifier {
#ViewBuilder
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content
.scrollIndicators(.hidden)
} else {
content
}
}
}
swift scrollview hide scrollbar
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
hide scroll view indicators bar swiftui
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}

In a SwiftUI AppLifecycle Document App, how can I get a menu command in active ContentView?

I'm writing a MacOS document app using the SwiftUI App lifecycle, and all the tricks I see here and elsewhere for sending a menu action to the active window depend on using platform specific implementation, which is (mostly) unavailable in a SwiftUI Lifecycle app. What I'm looking for is something like SideBarCommands(), which adds a menu item that, when selected by mouse or command key, toggles the appearance of the sidebar in the active window. All the Command examples I have seen thus far are trivial, none address a multi-document, multi-window use case.
Given a ContentView declared thusly:
struct ContentView: View {
#Binding var document: TickleDocument
var body: some View {
TextEditor(text: $document.text)
}
public func menuTickle() {
document.text = "Wahoo!"
}
}
and a command, which is added via:
struct TickleApp: App {
public static var target:TickleDocument?
var body: some Scene {
let docGroup = DocumentGroup(newDocument: TickleDocument()) { file in
ContentView(document: file.$document)
}
docGroup
.commands {
CommandMenu("App Tickles") {
Button("Tickle The ContentView") {
// Here's where I need to call menuTickle() on the active ContentView
}.keyboardShortcut("t")
}
}
}
}
}
What do I need to do so the button closure can call menuTickle() on the active ContentView? I know it can be done, because SideBarCommands() does it (unless Apple is using some non-public API to do it...).
For bonus points, tell me how I can detect whether or not I'm the active ContentView while body is being evaluated, and how I can detect when it changes! Tracking the Environment variable scenePhase is worthless - it always reports active, and never changes.
My question is a duplicate of this one.
The answer to that question contains a link to a solution that I have verified works, and can be found here

FirstResponder & :onCommit on TextField in SwiftUI

In a SwiftUI app, I need to set the focus on a TextField and bring the keyboard automatically, in standard Swift this would be done with:
field.becomeFirstResponder()
But this does not seem to exist in SwiftUI.
I found a work around here.
But, my field uses :onCommit; which is not in the sample code.
What is the way to set the :onCommit functionality when using UIViewRepresentable ?
iOS 15+ has a solution for this.
#FocusState combined with the focused(_:) modifier can be used to control first responder status for textfields.
struct ExampleView: View {
#FocusState private var isFocused: Bool
#State private var textInput = ""
var body: some View {
TextField("Example", text: $textInput)
.focused($isFocused)
Button("Confirm") {
if textInput {
isFocused = true
}
}
}
}
For iOS15
There is a solution implemented by apple (as mentioned by #AlphaWulf)
For iOS14
In my opinion, the best approach is to implement your own version of TextField using the UIRepresentable protocol. This might sound like something difficult but it is actually quite simple.
Why it is better to implement your own text field over the solutions using view hierarchy introspection?
One is that a solution based on traversing underlying views is hacky by nature and even a minor iOS version update might break it.
Secondly, in a real-world app, you will want to set additional things on the text field (like return button type and supplementary view) but Apple didn't make a way of doing so and you will be forced to wrap a UITextField in any case.
https://blog.agitek.io/swiftui-2-first-responder-b6a828243268
In this post I have a detailed solution that is similar to what Apple has implemented in SwiftUI 3.
There is an open-source project for your needs, at https://github.com/mobilinked/MbSwiftUIFirstResponder
TextField("Name", text: $name)
.firstResponder(id: FirstResponders.name, firstResponder: $firstResponder, resignableUserOperations: .all)
TextEditor(text: $notes)
.firstResponder(id: FirstResponders.notes, firstResponder: $firstResponder, resignableUserOperations: .all)

How can I "forward" the entire SwiftUI environment to another view?

It seems that certain SwiftUI views create new environment contexts, such as NavigationLink. None of the environment is available in the new view.
As a workaround, I've been just manually forwarding through environment variables, like this:
struct ExampleView: View {
#EnvironmentObject var foo: UserStore
#EnvironmentObject var bar: UserStore
var body: some View {
NavigationLink(destination:
SomeOtherView()
.environmentObject(self.foo)
.environmentObject(self.bar)
) {
Text("Open View")
}
}
}
However this seems broken, as it violates the purpose of the environment. Also, it's confusing because it's not clear (or documented?) where these boundaries are and it causes a runtime error when a view depends on a missing EnvironmentObject.
Is there a better way to do this?
Similarly, I want to create a wrapper UIViewControllerRepresentable that can contain SwiftUI children (via UIHostingController) and I would like those children to have access to the environment as well.

SwiftUI: how do I conditionally present ActionSheet only on iOS?

I’ve started to like an approach where I write crossplatform UI code with SwiftUI. The app would still be started with a native window/container, but have a fully crossplatform SwiftUI-driven UI. For many standard things things like list, navigationview etc, it is very useful and works fine.
The problem arises with some platform-specific view extensions. I would like to write this code in a platform-agnostic fashion, but not sure how to do it for some specific cases.
First, here’s a working example of a crossplatform conditional view modifier.
import SwiftUI
struct DemoView: View {
var body: some View {
Text("Hello, demo!").padding()
.modifier(iosBackground())
}
}
struct iosBackground: ViewModifier {
#if os(OSX)
func body(content: Content) -> some View {
content
}
#else
func body(content: Content) -> some View {
content.background(Color.blue)
}
#endif
}
What the iosBackground modifier is doing, is applying a view modification only on the iOS platform (well, to be specific, on any non-OSX platform, but let’s just work with OSX and iOS in this example). The OSX version of the view is passed through, while the iOS version returns a modified view. This color example is of course dumb and useless, but for layout-related things like padding, it is a highly practical approach.
My question: how do I apply the same approach to modifiers like actionSheet? Here’s what I would like to do:
struct DemoView: View {
#State var showActionSheet = true
var body: some View {
Text("Hello, demo!").padding()
.modifier(iosBackground())
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(
title: Text("Actions"),
message: Text("Available actions"),
buttons: [
.cancel { },
.default(Text("Action")),
.destructive(Text("Delete"))
]
)
}
}
}
If you try to compile this code, it works fine on iOS. On OSX, it has a compilation error because the actionSheet API is not available on OSX. Which, indeed, is the case. I would like to make it so that the actionSheet call would simply be a no-op on OSX, but I can’t figure out how to structure and conditionally compile my code to make it happen.
The question, once again: how can I structure this code so that on iOS, actionSheet would be presented, while on OSX, it would be a no-op?
You are almost there. You would have found the way if you took a look at the .actionSheet's function signature. It returns an opaque type of some View that is the return type of almost all the SwiftUI views. So, look at the documentation too:
/// Presents an action sheet.
///
/// - Parameters:
/// - isPresented: A `Binding` to whether the action sheet should be
/// shown.
/// - content: A closure returning the `ActionSheet` to present.
#available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
#available(OSX, unavailable)
public func actionSheet(isPresented: Binding<Bool>, content: () -> ActionSheet) -> some View
That being said, you could use this as like as you have used the .background function in conjunction with the content. So, here is the solution:
struct Sheet: ViewModifier {
#Binding var presented: Bool
func body(content: Content) -> some View {
#if os(OSX)
return content
#else
return content
.actionSheet(isPresented: $presented) {
ActionSheet(title: Text("Action Title"),
message: Text("Action Message"),
buttons: [.cancel(), .default(Text("Ok"))]
)
}
#endif
}
}
I just moved the #if - #endif inside the function body and that requires the return keyword explicitly. And you would use this as any modifier:
.modifier(Sheet(presented: $showActionSheet))