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

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))

Related

SwiftUI: PrivacySensitive doesn't work for iOS16

I would like to redact a text on my widget when the device is locked. The below code works when I test it on iOS15, but didn't work on iOS16.
struct WidgetView : View {
var body: some View {
Text("Some sensitive text")
.privacySensitive()
}
}
Above applies to testing on both simulator and actual devices. I use xcode 14.2

Why are my SwipeActions overflowing the listview SwiftUI

Why is my SwiftUI Swipe Action behaving like this?
I don't now how to add a GIF in stack overflow so here is a imagur link https://imgur.com/a/9MqjIgX.
If you don't want to click on external links here is a image from the GIF:
My View:
struct MyView: View {
#State var shops = [Shop.empty(), Shop.empty(), Shop.empty(), Shop.empty(), Shop.empty()]
var body: some View {
NavigationView {
List($shops) { $shop in
Text(shop.name)
.swipeActions {
Button {
shop.toggleFavourite()
} label: {
Image(systemName: "star")
}
}
}
}
}
}
the shop struct:
struct Shop: Hashable, Identifiable {
var id: UUID
var favourite: Bool
init(id: UUID){
self.id = id
self.favourite = UserDefaults.standard.bool(forKey: id.uuidString)
}
mutating func toggleFavourite() {
favourite.toggle()
UserDefaults.standard.set(favourite, forKey: id.uuidString)
}
static func empty() -> Shop{
Shop(id: UUID())
}
}
But I can't sadly I can't give you a working example, because I tried to run this code in a fresh app and it worked, without the Bug. On the same device. And I don't understand why, because I also put this view in the root of my old project, just for testing, and the bug stayed there.
I was able to figure out, that if I commented out this line:
UserDefaults.standard.set(favourite, forKey: id.uuidString)
my code would work. But unfortunately I can't just leave out this line of code.
I tried several things, including wrapping this line into DispatchQueue.main.async {} and DispatchQueue.main.sync {}, same with the DispatchQueue.global(). I also added delays. Short delays wouldn't work at all (under .5 seconds) and longer delays would just delay the view bug.
Of course I also tried wrapping this line into a separate function, and so on.
There are two mayor points, why I'am so confused:
Why is the line, that sets this to the Userdefaults even influencing the view? I mean I checked with a print statement, that the initializer, which is the only section in my code that checks this Userdefaultvalue, only gets called when the view gets initialized.
Why does the code work in a different project?
I know since I can't provide a working example of my bug it's hard for you to figure out whats wrong. If you have any ideas, I would be very happy!

How can I conform to ShapeStyle protocol in 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)
}
}

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
}

Create floating view with custom outline like in Notability

How to create such a view floating view with a custom boarder as shown in the picture? And such that it disappears as soon as the user clicks outside of the view.
Normally you would do that with a Popover like this:
#State var isPresented = false
var body: some View {
Button(action: {
self.isPresented = true
}) {
Text("Press me")
}.popover(isPresented: $isPresented, arrowEdge: .top) {
Text("Pop!") // You can put you own custom view here for the popover
}
}
Although it works as intended on the iPad (and I believe tvOS too, but I haven't tested it), it does not work properly with the current version of SwiftUI (as of 10/12/2019) on iPhones. Currently, the above code will just result in a somewhat glitchy modal on an iPhone, which I don't think is the intended function of it on iPhones. Apple's documentation for popover isn't very helpful right now, but here it is anyway.
For you information .popover is unabailable in tvOS.