I use the following code snippet (in Xcode 13 Beta 5 and deployment target set to 14.0) to apply view modifiers conditionally according to iOS version:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.modifyFor(iOS14: {
$0.onAppear {
//do some stuff
}
}, iOS15: {
$0.task { //<---- Error: 'task(priority:_:)' is only available in iOS 15.0 or newer
//do some stuff
}
})
}
}
struct CompatibleView<Input: View,
Output14: View,
Output15: View>: View {
var content: Input
var iOS14modifier: ((Input) -> Output14)?
var iOS15modifier: ((Input) -> Output15)?
#ViewBuilder var body: some View {
if #available(iOS 15, *) {
if let modifier = iOS15modifier {
modifier(content)
}
else { content }
}
else {
if let modifier = iOS14modifier {
modifier(content)
}
else { content }
}
}
}
extension View {
func modifyFor<T: View, U: View>(iOS14: ((Self) -> T)? = nil,
iOS15: ((Self) -> U)? = nil) -> some View {
CompatibleView(content: self,
iOS14modifier: iOS14,
iOS15modifier: iOS15)
}
}
this code works great as long as I don't use iOS 15's view modifiers, but if I want to use any of those modifiers (like Task for ex.) then I need to use the #available directive which's an option I don't wanna opt in, because my codebase is large, there are many parts that should adopt the new iOS 15 modifiers and by using #available everywhere in the code will make it looks like a dish of Lasagna.
how to make this piece of code compiles in a clean way and without using the #available check ?
The best solution for so far I've figured out is to add simple modify extension function for view and use that.
It's useful if availability check for modifier is needed only in one place.
If needed in more than one place, then create new modifier function.
public extension View {
func modify<Content>(#ViewBuilder _ transform: (Self) -> Content) -> Content {
transform(self)
}
}
And using it would be:
Text("Good")
.modify {
if #available(iOS 15.0, *) {
$0.badge(2)
} else {
// Fallback on earlier versions
}
}
EDIT:
#ViewBuilder
func modify<Content: View>(#ViewBuilder _ transform: (Self) -> Content?) -> some View {
if let view = transform(self), !(view is EmptyView) {
view
} else {
self
}
}
This allows us not to define fallback if not required and the view will stay untouchable.
Text("Good")
.modify {
if #available(iOS 15.0, *) {
$0.badge(2)
}
}
There is no way to do this without 'if #available', but there is a way to structure it in a somewhat clean way.
Define your own View Modifier on a wrapper View:
struct Backport<Content> {
let content: Content
}
extension View {
var backport: Backport<Self> { Backport(content: self) }
}
extension Backport where Content: View {
#ViewBuilder func badge(_ count: Int) -> some View {
if #available(iOS 15, *) {
content.badge(count)
} else {
content
}
}
}
You can then use these like this:
TabView {
Color.yellow
.tabItem {
Label("Example", systemImage: "hand.raised")
}
.backport.badge(5)
}
Blog post about it:
Using iOS-15-only View modifiers in older iOS versions
You can create a simple extension on View with #ViewBuilder
fileprivate extension View {
#ViewBuilder
var tabBarTintColor: some View {
if #available(iOS 16, *) {
self.tint(.red)
} else {
self.accentColor(.red)
}
}
}
To use it just have it chained with your existing view
TabView()
.tabBarTintColor
There is no point because even if you did back-port a modifier named task (which is how this problem is normally solved) you won’t be able to use all the magic of async/await inside which is what it was designed for. If you have a good reason for not targeting iOS 15 (I don’t know any good ones) then just continue to use onAppear as normal and either standard dispatch queue async or Combine in an #StateObject.
There is no logical use case for that modifier for the issue you are trying to solve! You have no idea, how many times your app would check your condition about availability of iOS15 in each render! Maybe 1000 of times! Insane number of control which is totally bad idea! instead use deferent Views for each scenarios like this, it would checked just one time:
WindowGroup {
if #available(iOS 15, *) {
ContentView_For_iOS15()
}
else {
ContentView_For_No_iOS15()
}
}
Related
I have a SwiftUI View for a watchOS / iOS app in which I use the #if os(xxxOS) compiler directive to select between a watchOS specific View or an iOS specific View to provide the content for the the OSCommonView body var. Both the watchOS and iOS body views call the same bunch of view modifiers, which themselves reference functions declared in the parent OSCommonView. So instead of replicating those modifiers in each of the iso/watchOS subviews I thought I could use the #if...#else...#endif compiler directives to switch-in the OS-specific subview view, followed by the modifiers. However, I get the error "Cannot infer contextual base for ..." for the first and last modifier, whatever modifier is the first or last. It seems that #endif makes a break such that the compiler does not "see" the optional view above the first modifier??
struct OSCommonView: View {
var body: some View {
#if os(watchOS)
body_watchOS
#else
body_iOS
#endif
.onAppear() { handleOnAppear() } <= Cannot infer contextual base error...
.onDisappear() { handleOnDisappear() }
.onReceive(uiUpdateTimer) { handleReceive_uiUpdateTimer(newDate: $0) }
/* --- several other modifiers --- */
}
var body_iOS: some View {
/* view subviews etc */
}
var body_watchOS: some View {
/* view subviews etc */
}
func handleOnAppear() { /* ... */}
func handleOnDisappear() { /* ... */}
func handleReceive_uiUpdateTimer(newDate: Date) { /* ... */}
}
I naively assumed the compiler directives were "invisible" and just presented the code seamlessly according to the switch. I have tried to create a ViewModifier to group them but failed to find a way to get the parameters (some with Bindings) and methods of the OSCommonView into a ViewModifier struct. At the moment I just duplicate the long list of view modifiers within each definition of body_watchOS and body_iOS.
If I put the modifiers inside the compiler switch above they work fine but that defeats the point of not duplicating them!
#if os(watchOS)
body_watchOS
.onAppear() { handleOnAppear() }
.onDisappear() { handleOnDisappear() }
...
#else
body_iOS
.onAppear() { handleOnAppear() }
.onDisappear() { handleOnDisappear() }
...
#endif
Any suggestions? Many thanks
You can wrap those kinds of directives in closures, while waiting for the missing feature that would negate the need for them:
var body: some View {
{
#if os(watchOS)
body_watchOS
#else
body_iOS
#endif
} ()
.onAppear(perform: handleOnAppear)
However, it's not obvious why you don't just do this:
var body: some View {
_body
.onAppear(perform: handleOnAppear)
#if os(watchOS)
private var _body: some View {
…
}
#else
private var _body: some View {
…
}
#endif
I'm trying to apply some validation to a TextField to add a red border around the field when the content is invalid (in this case, I'm validating that the content is a positive number that is less than the specified maxLength).
The validation works fine and the border is applied when the value is out of range. However, when the border is applied to the TextField, the TextField loses focus in the UI (and also loses focus when the border is removed).
Here is a snippet of my code (I've included some extensions I'm using, but I don't think those are relevant to the issue)
import Foundation
import SwiftUI
import Combine
struct MyView : View {
#Binding var value: Int
var label: String
var maxLength: Int
#State private var valid: Bool = true
var body: some View {
TextField(label, value: $value, format: .number)
.textFieldStyle(RoundedBorderTextFieldStyle())
.fixedSize()
.multilineTextAlignment(.trailing)
.onReceive(Just(value), perform: validate)
.if(!valid, transform: { $0.border(.red)})
}
func validate(val: Int) {
let newVal = val.clamped(to: 0...maxLength)
if newVal != val {
valid = false
} else {
valid = true
}
}
}
extension View {
#ViewBuilder
func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
if condition { transform(self) }
else { self }
}
}
extension Comparable {
func clamped(to limits: ClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
}
}
Is there any way to preserve focus on the TextField when styles are conditionally applied to it?
Or am I approaching validation wrong altogether? Is there a better way to check fields and apply conditional styling?
Because of the way your if modifier is structured, SwiftUI is unable to see that the underlying View is the same in the two conditions. For more detail on this, I'd suggest you watch Demystifying SwiftUI from WWDC 2021.
One solution is the simplify your border modifier into the following:
.border(valid ? .clear : .red)
This way, SwiftUI can still tell that this is the same underlying View.
I am working on a CustomForEach which would act and work like a normal ForEach in SwiftUI, this CustomForEach has it own early days and it has some issues for use for me, which makes me to learn more about SwiftUI and challenge me to try to solve the issues, one of this issues is finding a way to destroy the unneeded Views instated of rendering all needed Views!
Currently when I update lowerBound the CustomForEach starts rendering for new range which is understandable. But the new range need less Views than before and that is not understandable to rendering them again for already rendered Views.
Goal: I want find a way to stop rendering all needed Views because they are already exist and there is no need to rendering again, and just removing the unneeded Views. And also I do not want start an another expensive calculation inside CustomForEach for finding out if the Views already exist!
struct TextView: View {
let string: String
var body: some View {
print("rendering " + string)
return HStack {
Text(string)
Circle().fill(Color.red).frame(width: 5, height: 5, alignment: .center)
}
}
}
struct CustomForEachView<Content: View>: View {
private let id: Int
let range: ClosedRange<Int>
let content: (Int) -> Content
init(range: ClosedRange<Int>, #ViewBuilder content: #escaping (Int) -> Content) {
self.id = range.lowerBound
self.range = range
self.content = content
}
// The issue is rendering all existed Views when lower Bound get updated, even we do not need to render new View in updating lower Bound!
var body: some View {
content(range.lowerBound)
if let suffixRange = suffix(of: range) {
CustomForEachView(range: suffixRange, content: content)
}
}
private func suffix(of range: ClosedRange<Int>) -> ClosedRange<Int>? {
return (range.count > 1) ? (range.lowerBound + 1)...range.upperBound : nil
}
}
struct ContentView: View {
#State private var lowerBound: Int = -2
#State private var upperBound: Int = 2
var body: some View {
HStack {
CustomForEachView(range: lowerBound...upperBound) { item in
TextView(string: item.description)
}
}
HStack {
Button("add lowerBound") { lowerBound += 1 }
Spacer()
Button("add upperBound") { upperBound += 1 }
}
.padding()
}
}
First of all, one thing important thing to understand is that a SwiftUI.View struct is not a view instance that is rendered on the screen. It's merely a description of the desired view hierarchy. The SwiftUI.View instances are going to be recreated and torn down a lot by the framework anyway.
The SwiftUI framework takes care of the actual rendering. It might use UIViews for this, or it might not. That's an implementation detail you shouldn't need to worry about in most cases.
That said, you might be able to help the framework by adding explicit ids to the views by using the id modifier. That way SwiftUI can use that to keep track of which view is which.
But, I'm not sure if that would actually help. Just an idea.
Gif to understand easier
Is there any way to disable collapsibility of SidebarListStyle NavigationViews?
EDIT: This method still works as of late 2022, and has never stopped working on any version of macOS (up to latest Ventura 13.1). Not sure why there are answers here suggesting otherwise. If the Introspection library changes their API you may need to update your calls accordingly, but the gist of the solution is the same.
Using this SwiftUI Introspection library:
https://github.com/siteline/SwiftUI-Introspect
We can introspect the underlying NSSplitView by extending their functionality:
public func introspectSplitView(customize: #escaping (NSSplitView) -> ()) -> some View {
return introspect(selector: TargetViewSelector.ancestorOrSibling, customize: customize)
}
And then create a generic extension on View:
public extension View {
func preventSidebarCollapse() -> some View {
return introspectSplitView { splitView in
(splitView.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
}
}
}
Which can be used on our sidebar:
var body: some View {
(...)
MySidebar()
.preventSidebarCollapse()
}
The introspection library mentioned by Oskar is not working for MacOS.
Inspired by that, I figured out a solution for MacOS.
The rationality behind the solution is to use a subtle way to find out the parent view of a NavigationView which is a NSSplitViewController in the current window.
Below codes was tested on XCode 13.2 and macOS 12.1.
var body: some View {
Text("Replace with your sidebar view")
.onAppear {
guard let nsSplitView = findNSSplitVIew(view: NSApp.windows.first?.contentView), let controller = nsSplitView.delegate as? NSSplitViewController else {
return
}
controller.splitViewItems.first?.canCollapse = false
// set the width of your side bar here.
controller.splitViewItems.first?.minimumThickness = 150
controller.splitViewItems.first?.maximumThickness = 150
}
}
private func findNSSplitVIew(view: NSView?) -> NSSplitView? {
var queue = [NSView]()
if let root = view {
queue.append(root)
}
while !queue.isEmpty {
let current = queue.removeFirst()
if current is NSSplitView {
return current as? NSSplitView
}
for subview in current.subviews {
queue.append(subview)
}
}
return nil
}
While the method that Oskar used with the Introspect library no longer works, I did find another way of preventing the sidebar from collapsing using Introspect. First, you need to make an extension on View:
extension View {
public func introspectSplitView(customize: #escaping (NSSplitView) -> ()) -> some View {
return inject(AppKitIntrospectionView(
selector: { introspectionView in
guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
return nil
}
return Introspect.findAncestorOrAncestorChild(ofType: NSSplitView.self, from: viewHost)
},
customize: customize
))
}
}
Then do the following:
NavigationView {
SidebarView()
.introspectSplitView { controller in
(controller.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
}
Text("Main View")
}
This being said, we don't know how long this will actually work for. Apple could change how NavigationView works and this method may stop working in the future.
I'm trying to update my MVVM-Coordinators pattern to use it with SwiftUI and Combine.
To preserve abstraction, I use a ScenesFactory that handle the creation of, well, my scenes like the following:
final class ScenesFactory {
let viewModelsFactory = SceneViewModelsFactory()
}
extension ScenesFactory: SomeFlowScenesFactory {
func makeSomeScene() -> Scene {
let someSceneInput = SomeSceneInput()
let someSceneViewModel = viewModelsFactory.makeSomeSceneViewModel(with: someSceneInput)
let someSceneView = SomeSceneView()
someSceneView.viewModel = someSceneViewModel
return BaseScene(view: someSceneView, viewModel: someSceneViewModel)
}
}
Here is the implementation of a my Scene protocol:
public protocol Scene {
var view: some View { get }
var viewModel: ViewModelOutput { get }
init(view: some View, viewModel: ViewModelOutput)
}
The goal here is to be able to use UIHostingControllerto present my someScene.view but the compiler throws an error at my Scene protocol:
I thought the point of the some keyword was precisely to use generic protocols as a return type.
What am I missing ?
I thought the point of the some keyword was precisely to use generic protocols as a return type.
Yes, but it seems that it's doesn't work that way in a protocol declaration, not really sure why.
But there is a way to fix this, use an associatedtype that is constrained to View, and the compiler stop complaining.
public protocol Scene {
associatedtype Content: View
var view: Content { get }
}
struct V: Scene {
var view: some View {
EmptyView()
}
}